GreenSQLでMySQLデータベースをSQLインジェクション攻撃から守る

 SQLインジェクションとは、Webサイト経由で不正なSQLコマンドをデータベースに対して実行するという攻撃手法だ。こうした攻撃を防ぐには、ユーザがWebフォームに入力し、HTTP POSTやCGIパラメータなどによって送られてくるすべてのデータについて、想定外の情報が含まれないことをチェックする必要がある。GreenSQLはSQL用のファイアウォールである。WebサイトとMySQLデータベースの間に置かれ、実行すべきSQL文とそうでないSQL文を判別する。考え方としてはSQLインジェクション対策にうってつけのものだが、実行してみるといくつか抜け穴が見つかった。

 GreenSQLは、MySQLデータベースのプロキシとして利用するように作られている。MySQLデータベースとWebサイトを直接つながずに、GreenSQLを間に置くわけだ。GreenSQLは、正当なSQLをMySQLデータベースに転送し、その結果を返す。ホワイトリストになく、かつ危険または疑わしい命令を含むSQLを検出した場合は、そのSQLをブロックし、MySQLデータベースとのやりとりを行わずに空の結果を返す。GreenSQLによるSQLインジェクション攻撃の阻止については、デモ用のページがWebに用意されている。

 GreenSQLは、Fedora、openSUSE、Ubuntuのいずれのリポジトリにも入っていないが、Fedora 8とopenSUSE 10.3には、ワンクリックでインストール可能なパッケージがある。また、GreenSQLのダウンロードページには、Fedora 7、openSUSE 10.2、FreeBSD、Feisty Fawn以前のUbuntuに対応したパッケージが用意されている。ここでは、greensql-fwのバージョン0.8.4のインストールを64ビット版Fedora 9マシンで行う。

 autotoolsはインストールに使われておらず、設定ファイル、システムユーザ、MySQL、ログファイル、「/etc/init.d」ファイルなどの設定は自力で行うことになるが、その手順は「install.txt」にしっかりと記されている。また、こうした設定の大半は、「scripts」ディレクトリにあるいくつかのシェルスクリプトの実行で済ませることができる。コンパイルは、トップディレクトリ内でmakeを実行するだけでよい(下記のコマンドを参照)。私のFedora 9では、コンパイルを開始してすぐに次のエラーが出たが、これはlibevent-develがインストールされていないためだった。

$ tar xzf /.../greensql-fw-0.8.4.tar.gz
$ cd greensql-fw-0.8.4/
$ make
...
connection.hpp:29: error: field 'proxy_event' has incomplete type
connection.hpp:30: error: field 'client_event' has incomplete type

 libevent-develをインストールしたあと、イベントコードをコンパイルするために、次のように「/usr/include/event.h」を編集して「sys/types.h」をインクルードする必要があることに気付いた。

vi /usr/include/event.h
...
#include <sys/time.h>
#include <sys/types.h>
#include <stdint.h>
#include <stdarg.h>

 この段階でも、ヘッダファイル「string.h」をインクルードしていなかったため、strcasecmpのような文字列関数を呼び出すいくつかのファイルで「make -k」の実行に失敗した。利用するgccのバージョンによっては、こうした問題がGreenSQLのコンパイル時に起こらない場合もある。

gccの問題
 本文に書いたようなビルドの問題が生じるのは、「string.h」内の特定の関数を利用するコードのコンパイル時にgccのチェックがかなり厳しくなっているためだ。以前はこのヘッダファイルをインクルードしなくてもstrlenが使えたが、今はこれをインクルードしないとgccによるコンパイルができない。

 また、64ビット版のライブラリパスの変更は、そうした変更を自動的に処理するautotoolsのようなシステムによる検出方法が使われていないこと、Makefileが32ビット版のディストリビューション向けに作られていることに起因する。どちらもあまり注目されていないが、GreenSQLを64ビット版Fedora 9マシンにインストールしようとする場合には面倒な事態を引き起こす。
$ cd src
$ vi mysql/mysql_con.cpp
...
// License: GPL v2 (http://www.gnu.org/licenses/gpl.html)
//

#include <string.h>
#include "mysql_con.hpp"
...

$ vi config.hpp
...
#ifndef GREEN_SQL_CONFIG_HPP
#define GREEN_SQL_CONFIG_HPP

#include <string.h>
...

$ vi ../src/parser/expression.hpp
...
#ifndef _SQL_EXPRESSION_HPP_
#define _SQL_EXPRESSION_HPP_

#include <string.h>
...

 GreenSQLのビルドを64ビット版のディストリビューションで行う場合は、次のように、ビルド時に「lib」ディレクトリだけでなく「lib64」ディレクトリもチェックするようにMakefileの変更も行う必要がある。

$ vi src/Makefile
...
LIBS:=-L/usr/local/lib -L/usr/local/lib/mysql -L/usr/lib64/mysql -lmysqlclient -levent -lpcre

greensql-fw: $(OBJS)
...
$ make

 また、デーモンのコンパイル後にインストール作業を終了させるためのコマンド群を以下に示す。パッケージの「install.txt」ファイルには、このスクリプトにあるのと同じコマンドを使った手動インストールの方法が記されている。やっていることは同じでもインストール処理(つまり、スクリプトの中身)を拡張できるという点から、こうしたスクリプトを使ったほうがよいだろう。インストール手順では、「/etc/greensql」ディレクトリの前にMySQLデータベースのセットアップを行うことを推奨しているが、そうするとMySQLスクリプトを実行してもGreenSQLの設定ファイルが見つからず、「/etc/greensql/greensql.conf」ファイルを変更しなければならなくなる。これはちょっとした「鶏が先か卵が先か」問題だが、「greensql.conf」ファイルを変更してMySQLデータベースのパラメータを取得するようにしてやれば解決できる。

greensql-fw-0.8.4]# cd ./scripts/
# ./setup_user.sh
done...
# ./greensql-create-db.sh

---------------------------------------------
The following settings will be used:

MySQL admin user: [root]
MySQL admin password: []
MySQL server address: [127.0.0.1]

GreenSQL configuration DB name: [greendb]
DB user to create: [green]
Password to set: [pwd]

Do you want to change anything? [y/N] y

MySQL admin user [root]:
MySQL admin password []: XXxxXXxxXXxx-FIXME
MySQL server address (you can use ip:port string) [127.0.0.1]:
GreenSQL config db name [greendb]:
GreenSQL DB user name [green]: greendb
GreenSQL DB user password [pwd]: greendbpass
---------------------------------------------
The following settings will be used:
Do you want to change anything? [y/N]


Creating MySQL database...
Adding MySQL user...
Creating MySQL tables...

GreenSQL configuration file is not writable!!!
Check that [database] section contains the following settings in
/etc/greensql/greensql.conf

[database]
dbhost=127.0.0.1
dbname=greendb
dbuser=greendb
dbpass=greendbpass
# dbport=3306
...
# ./setup_conf.sh
done...
# ./setup_log.sh
done...
# ./setup_binary.sh
done...
# vi /etc/greensql/greensql.conf
...
[database]
dbhost=127.0.0.1
dbname=greendb
dbuser=greendb
dbpass=greendbpass
...
# chkconfig  --add greensql
service greensql does not support chkconfig

# /etc/init.d/greensql start

 以下のコマンドを使って、テスト用のデータベースtestを作成し、ユーザbenが自由にアクセスできるようにした。

# mysql -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
mysql> GRANT ALL ON test.* TO ben@"%";
mysql> FLUSH PRIVILEGES;

 デフォルトだと、GreenSQLは3305番ポート(MySQLのポート番号3306より1つ小さい)で動作する。mysqlコンソールクライアントを使ってGreenSQLの3305番ポートに接続すると、3306番ポートでMySQLに直接接続した場合には可能なテーブルの作成が行えなくなる(以下を参照)。

$ mysql --verbose  -h  127.0.0.1 -P 3305 test
mysql> create table foo ( id int );
--------------
create table foo ( id int )
--------------

Query OK, 0 rows affected (0.01 sec)

mysql> insert into foo values ( 55 );
--------------
insert into foo values ( 55 )
--------------

ERROR 1146 (42S02): Table 'test.foo' doesn't exist


$ mysql --verbose  -h  127.0.0.1   -P 3306 test
Welcome to the MySQL monitor. Commands end with ; or \g.
mysql> create table foo ( id int );
--------------
create table foo ( id int )
--------------

Query OK, 0 rows affected (0.01 sec)

mysql> insert into foo values ( 55 );
--------------
insert into foo values ( 55 )
--------------

Query OK, 1 row affected (0.00 sec)

mysql> insert into foo values ( 131 );
--------------
insert into foo values ( 131 )
--------------

Query OK, 1 row affected (0.00 sec)

mysql>  select * from foo;
--------------
select * from foo
--------------

+------+
| id   |
+------+
|   55 |
|  131 |
+------+
2 rows in set (0.00 sec)

 GreenSQLのデフォルト設定では、GreenSQLファイアウォール経由でテーブルを破棄することもできない。というのも、テーブル構造が変更されることはあまりなく、ましてWebサイトのインタフェースからそうした操作が行われることはほとんどありえないからだ。

$ mysql --verbose  -h  127.0.0.1   -P 3305 test
Welcome to the MySQL monitor. Commands end with ; or \g.

mysql>  select * from foo;
--------------
select * from foo
--------------

+------+
| id   |
+------+
|   55 |
|  131 |
+------+
2 rows in set (0.00 sec)

mysql> drop table foo;
--------------
drop table foo
--------------

Query OK, 0 rows affected (0.00 sec)

mysql>  select * from foo;
--------------
select * from foo
--------------

+------+
| id   |
+------+
|   55 |
|  131 |
+------+
2 rows in set (0.01 sec)

 早速、インジェクションのテストを行った。だが残念ながら、期待どおりの結果にはならなかった。最初のテストは、常に真となる条件でタプルを削除するというものだ。これが実行されると、テーブル内のすべてのデータが削除され、空のテーブルスキーマだけが残ることになる。しかしデフォルトでは、このクエリがGreenSQLファイアウォールを通ってしまった。

$ mysql --verbose  -h  127.0.0.1   -P 3305 test

mysql> delete from foo where 1=1;
--------------
delete from foo where 1=1
--------------

Query OK, 2 rows affected (0.00 sec)

mysql>  select * from foo;
--------------
select * from foo
--------------

Empty set (0.00 sec)

 「/var/log/greensql.log」ファイルには、上記のSQL削除コマンドに関する情報が次のように記録されていた。

SQL_DEBUG: QUERY command[]: delete from foo where 1=1
SQL_DEBUG: AFTER NORM   : delete from foo where ?=?
SQL_DEBUG: RISK         : 0

 「/etc/greensql/greensql.conf」ファイルには、特定の項目に対してその危険度を設定することができる。たとえば、クエリにおけるunionキーワードの利用や変数の直接比較(「1=2」など)には、危険度を10にする。「block_level = 30」のように変数を設定することで、危険度が30を超えるクエリはMySQLサーバに転送されなくなる。先ほどのクエリをGreenSQLによって弾くために「risk_var_cmp_var」と「risk_always_true」の値をデフォルトの30から150に上げてみたが、それでもこのクエリの危険度はゼロと見なされた。

 上記のSQLは有効なものかもしれないので、GreenSQLのWebサイトにあるMySQLのSQLインジェクションに関するWebページに記載のテンプレートにあるクエリを利用した(下記のログを参照)。このSQLインジェクションは、ユーザが入力したデータをSQLクエリ文字列に挿入し、データベースに送信するWebサイトを利用したものだ。ところが、これもGreenSQLを素通りしてしまった。

SQL_DEBUG: QUERY command[]: delete from foo where id=181 or 1=1
SQL_DEBUG: AFTER NORM   : delete from foo where id=? or ?=?
SQL_DEBUG: RISK         : 0

SQL_DEBUG: QUERY command[]: delete from s where comment = 'whatever' or '1'='1'
SQL_DEBUG: AFTER NORM   : delete from s where comment = ? or ?=?
SQL_DEBUG: RISK         : 0

 strace(1)などを駆使してGreenSQLがなぜこんな危険なクエリの危険度をゼロと判断したかの解明を試みたあとで、SELECTクエリを試せばよいことに気付いた。GreenSQLを期待どおりに動作させ、以下のログファイルに示すように、この不正なクエリをブロックさせるための鍵はこれだったのだ。

SQL_DEBUG: QUERY command[]: select * from folks where name='sam' or '1'='1'
SQL_DEBUG: AFTER NORM   : select * from folks where name=? or ?=?
DEBUG:     Query has 'or' token
DEBUG:     Variable comparison only
SQL_DEBUG: RISK         : 35

 select文のSQLインジェクションにより、ユーザがWebサイトにパスワードなしでログインできることを考えれば、GreenSQLに選択操作のチェックを行わせることは確かに有効である。しかし、今後のリリースでは、GreenSQLによる保護をdelete文にも拡張してもらいたいと感じた。それらの1つにインジェクションをしかけることでテーブル全体を消去することも可能だからだ。

Ben Martinは10年以上もファイルシステムに携わっている。博士号を持ち、現在はlibferris、各種ファイルシステム、検索ソリューションを中心としたコンサルティングサービスを提供している。

Linux.com 原文(2008年8月25日)