ここからは、MongoDB以外のNoSQLを使うアプリケーションのセキュリティについて簡単に説明します。
前々回、前回の2回に分けて、NoSQLのうち「MongoDB」を用いたWebアプリケーションの脆弱性と対策について紹介してきました。
ここからは、MongoDB以外のNoSQLを使うアプリケーションのセキュリティについて簡単に説明します。取り上げるのは、Cassandra、Redis、memcachedの3つのNoSQLデータベースです。まずはCassandraについて見ていきます。
Cassandraは列指向型に分類されるNoSQLデータベースで、大容量のデータを高速に処理することに主眼を置いた製品です。元々はFacebookで開発されたデータベースですが、現在はApacheプロジェクトの一部としてオープンソースになっています。執筆時点の最新バージョンはv1.2.1で、本記事の内容はその挙動に基づきます。
Cassandraについては、バージョン0.8以降でCQL(Cassandra Query Language)というクエリ言語をサポートするようになりました。CQLはSQLと同等の構文を持つ言語であり、CQLを使うアプリケーションに不備があるならば、SQLインジェクションと似たような攻撃が可能です。
下記は、JavaとCassandra、CQLを使ってセッション情報を管理するプログラムの例です。この例では、JavaとCassandraの連携にJDBC(cassandra-jdbc)を使用しているため、SQLを使うデータベースアプリケーションと同様のスタイルのプログラムになっています。
// リクエストパラメータからセッションIDを取得 String sessionId = request.getParameter("sessionId"); // セッションDBを検索するためのCQLを生成・実行 String cql = "SELECT * FROM sessions WHERE key='" + sessionId + "'"; ResultSet rs = con.createStatement().executeQuery(cql); if (rs.next()) { // ログイン済みユーザーに対する処理 String userId = rs.getString("user_id"); ....
このプログラムは、セッションIDを入力パラメータとして受け取り、それをキーにセッションテーブル(sessions)を検索します。セッションテーブルに該当行が存在する場合には、ログイン済みユーザーとみなします。
このプログラムに対する正常なパラメータ例を下記に示します。
正常パラメータ例: sessionId=sess_b4c28bf0c8da17f88ff6a4ea1b91285a
SQLインジェクションに関する知識がある方ならばお分かりだと思いますが、このプログラムには、文字列連結でCQL文を作成しているという問題があります。この問題を突く攻撃の例を下記に示します。攻撃の目標は、正しいセッションIDをサーバに送ることなく、別の会員になりすますことです。
操作パラメータ例: sessionId=' AND user_id='victim
この操作により実行されるCQL文は下記となります。
SELECT * FROM sessions WHERE key='' AND user_id='victim'
cassandra-jdbc(バージョン1.2.1)で検証したところ、「key=''」が条件として無視されてしまい(※注1)、「user_id='victim'」の条件のみに合致する行が取得されました(ここでは、user_idにインデックスが設定されていると仮定しています)。こうなると、セッションIDを推測するまでもなく、user_idの推測のみでなりすましができることになります。
なりすまし以外の被害としては、データベース内のデータの窃取が思い付くところですが、これは一筋縄ではいきません。というのもCQLには、
などの機能上の強い制限が存在するからです。SQLインジェクション攻撃においては、SQLに備わるさまざまな機能を駆使してデータを窃取しますが、少なくとも現行のCQLの機能は強く制限されているため、CQLインジェクションを悪用してデータを窃取するなどの攻撃が可能なケースは限られます。
通常のSQLインジェクションと同じく、プレースホルダの使用が対策となります。JDBCを使用している際の具体的なプログラム例は下記の通りです。
String cql = "SELECT * FROM sessions WHERE key=?"; PreparedStatement stmt = con.prepareStatement(cql); stmt.setString(1, sessionId); ResultSet rs = stmt.executeQuery();
注1:WHERE句内の「key=''」が無視される現象は、phpcassa(PHPプログラムからCassandraにアクセスするためのライブラリ。バージョン1.0.a.6)を使ったPHPプログラムでは発生しませんでした。phpcassaでは空文字列のキーはエラーを発生させます。しかし、phpcassaにおいても、状況によってはSELECT文のWHERE条件の一部が評価されない現象が発生しました。マニュアルなどに詳細な情報がないため、バグかどうかは不明ですが、現状のCQLは動作の一貫性が不十分であるように思えます。
Redisは、Key-Value型に分類されるオープンソースのNoSQLデータベースです。リスト型/集合型など多くのデータ型を扱えること、揮発性のKey-Value型システムとは異なりデータの永続化(ディスクへの保存)が可能なことなどが特徴です。執筆時点の最新バージョンは2.6.10で、本記事の内容はその挙動に基づきます。
Redisはバージョン2.6.0からLuaスクリプトをサポートしています。これによって、より高度で複雑なデータ操作が可能となる一方で、MongoDBのSSJI(Server Side JavaScript Injection)に似たインジェクション攻撃のリスクがあります。
下記は、Luaスクリプトを使った関数(コマンド)を定義しているPHPのプログラムの一部です。
// Luaスクリプトのコマンドを定義するクラス class LeftPadding extends ScriptedCommand { public function getScript() { return <<<LUASCRIPT local pad_length = {$_GET['pad_length']} local str = ARGV[1] while string.len(str) < pad_length do str = " " .. str end return str LUASCRIPT; } } // 上記のLuaを使ったleftpadというコマンドをRedisに登録 $client->getProfile()->defineCommand('leftpad', 'LeftPadding'); // 登録したコマンドを実行する $client->leftpad('abc');
このプログラムを実行すると、Redisデータベースにleftpadというコマンド(関数)が作成されます。プログラム内の「<<<LUASCRIPT」と「LUASCRIPT;」の間の行がLuaスクリプトです。通常、Luaスクリプトにはデータベースの参照/更新処理などを実施させますが、上記プログラムではそういった処理は省いています。
ポイントは、Luaスクリプト内に、入力パラメータ「pad_length」の値を埋め込んでいる部分です。正常なパラメータの値は下記のような数値です。
正常パラメータ例: pad_length=20
Luaスクリプト内に入力パラメータの値をそのまま埋め込んでいるため、パラメータを操作することで、攻撃者は任意のLuaスクリプトをRedisサーバ上で実行させることができます。さまざまな攻撃が可能ですが、まずはDoS(サービス利用妨害)攻撃の例を3つ示します。
操作パラメータ例(A): pad_length=20; while true do end
操作パラメータ例(B): pad_length=20; redis.call("SHUTDOWN")
操作パラメータ例(C): pad_length=20; redis.call("FLUSHALL")
(A)のパラメータは無限ループを利用しています。RedisのLuaスクリプトは、MongoDBのJavaScriptと同じくシングルスレッドで実行されるため、他のリクエストも含めてブロックされてしまいます。(B)はRedisのSHUTDOWNコマンドを実行してRedisサーバプロセスを停止させています。(C)はRedisのデータをすべて削除します。
データベース内のデータを窃取・改ざんする攻撃も可能です。下記はデータを窃取する攻撃の例です。SQLインジェクションの攻撃手法にブラインドSQLインジェクションというものがありますが、攻撃の原理はそれと同じです。
操作パラメータ例: pad_length=20; if string.sub(redis.call("GET","foo"),1,1)=="a" then return 1 else redis.call("x") end
redis.call("GET","foo")によってキー「foo」の値を取得し、その1文字目をsub(substring)によって切り出しています。「foo」の値の1文字目が「a」の場合は1をreturnし、そうでなければ存在しないコマンド「x」が実行され、内部エラーが発生します。HTTP応答の差から、1がreturnされたのか、それとも内部エラーが発生したのかが推測できるならば、上記の操作パラメータによって、値の1文字目に関する情報(「a」なのか、そうでないのか)が得られます。
同様のリクエストを多く送ることで、値を特定できます。詳細は割愛しますが、応答にRedisのエラーメッセージが出力される状況であれば、より簡単に情報を窃取することもできます。
なお、MongoDBと同様に、RedisでもOSコマンドの実行やファイルへのアクセスを行う関数などは使用できないようにされています。ただしdofile関数を通じて限定的ながらファイルシステムにアクセスできる問題が知られています。
Filesystem Enumeration using Redis and Lua - Lift Security
http://blog.liftsecurity.io/post/35714931664/filesystem-enumeration-using-redis-and-lua
Luaスクリプト文字列に値を埋め込まないようにします。
具体的な方法はいくつか考えられますが、引数の形でLuaスクリプトに対して値を渡す仕組みがあるため、それを利用するのがよいでしょう。先に挙げた脆弱なアプリケーションの例においても、1つの引数(str変数)をPHP側から受け渡しており、他の変数(pad_length)もそれに倣えばよいということになります。Luaスクリプト側では、ARGVまたはKEYSから引数を取得することができます。
Copyright © ITmedia, Inc. All Rights Reserved.