※ 本記事は2017年6月15日に公開されたものです。

今回はSQLインジェクションのお話です。
SQLインジェクションが起きるサンプルページ自体はさすがに公開できないので、画面イメージだけになってしまいますが、どんなことが起きるのか、どうすれば防げるのかを説明します。

まず、サンプルのページの紹介です。今回、脆弱なWEBアプリケーションとして、検索で件数絞り込みができるJavaの住所録アプリを用意しました。

内部的は以下のようにSQLを発行して、条件に入れた文字がinStrに入って絞り込みをおこないます。

Statement stmt = conn.createStatement();
ResultSet rset = stmt.executeQuery("select name from dbsecdemo.addrbook where name like '%" + inStr + "%'");

例えば、「田」と条件に入れると以下の条件のSQL文が生成され、結果として以下のようなページが生成されます。

select name from dbsecdemo.addrbook where name like '%田%'

(2017年6月26日追記) このSQLだと、LIKE検索のワイルドカード文字である半角パーセント「%」や半角アンダーバー「_」を文字列として認識してくれません。この文章後半のSQLインジェクション対策ができているほうではきちんとこれらの文字をエスケープしていますが、こちらではエスケープしていませんでした。これらの文字をエスケープした書き方だと以下のようになります。

Statement stmt = conn.createStatement();
ResultSet rset = stmt.executeQuery("select name from dbsecdemo.addrbook where name like '%" + inStr.replaceAll("\\\\","\\\\\\\\").replaceAll("%","\\\\%").replaceAll("_","\\\\_") + "%' escape '\\'");

さて、実際に攻撃してみましょう。まず、ユーザーがアクセスできる表一覧を取得します。WEBアプリケーションの検索条件に下記の文字列を入力します。

xxxxxxxx' union select table_name from user_tables --

発行されるSQL文は以下のようになります。

select name from dbsecdemo.addrbook where name like '%xxxxxxxx' union select table_name from user_tables --%'

ここで、union句より前のSQL文は、「name like ‘%xxxxxxxx’」に合致するレコードはないため結果を戻さず、最後の「–%’」部分は、「–」はそれ以降はコメントとして扱うため無視されます。つまり、真ん中のselect table_name from user_tablesの結果が戻ります。

今回はADDRBOOK、APEX$TEAM_DEV_FILES、PRIVSという3つの表があることが分かります。今回は攻撃対象をADDRBOOK表とします。この表にどのような列があるのかを調べるために、WEBアプリケーションの検索条件に下記の文字列を入力します。

xxxxxxx' union select column_name from user_tab_columns where table_name='ADDRBOOK' --

ここでも同様にADDRBOOK表の列名を戻すselect column_name from user_tab_columns where table_name=’ADDRBOOK’の結果が戻ります。

ここまでで攻撃者は表の構造が分かったので、最後にデータを取得するSQL文を含んだ下記の文字列をWEBアプリケーションの検索条件に入力します。

xxxxx' union select name || ', ' || address || ', ' || phone || ', ' || birthday from addrbook --

本来アプリからは検索できないはずの情報まで画面に表示されてしまっています。

実際の攻撃ではもっとさまざまな内部構造を調査する必要があるでしょうが、基本的には上記のような問い合わせを繰り返して攻撃をおこない情報を搾取します。

SQLインジェクションの原因は、検索条件に入力した文字列をWEBアプリケーションが適切に処理していないため、データベースでSQLコマンドとして認識され、実行してしまうことです。今回のWEBアプリケーションではSQL文を作成する際に事前に用意した文字列とユーザーが検索条件として入力した文字列を単純に連結して、連結した文字列をデータベースにSQLコマンドとして送信して実行したことにあります。

ではどのWEBアプリケーションでSQLインジェクションが起きないようにするにはどうしたらよいでしょうか?
確実な方法は変数はプレースホルダとしてあとから指定し、SQL構文を事前にコンパイルしておくことです。簡単に言うと、PreparedStatementの利用を徹底することです。しかし、既存アプリケーションでPreparedStatementの利用が徹底されているかどうかをすべて確認することは難しいので、ここでは100%確実とは言えませんがSQLインジェクションが発生する可能性のあるページをあぶりだす方法の一つを紹介します。なお、この方法はペネトレーションテストでも利用しますが、実際のSQLインジェクション攻撃のための事前調査でも利用できる方法ですので、悪用はしないでください。

やることは簡単で、WEBアプリケーションの変数に半角シングルクォート「’」を入力します。

以下のようなSQL文が生成されますが、エラーとなってしまいます。

select name from dbsecdemo.addrbook where name like '%'%'

画面からはORA-00911エラーは発生します。同じSQLをSQL*Plusから実行するとORA-01756が発生します。

SQL> select name from dbsecdemo.addrbook where name like '%'%';
ERROR:
ORA-01756: 引用符付き文字列が正しく終了していません

半角のシングルクォートは、文字の区切りに利用する文字です。この文字が正しく処理されないということはSQLインジェクションを起こす可能性があるサイトといえます。

ちなみにSQLインジェクション攻撃がちらほら出始めた2005年、ORA-01756やほかのデータベースで同様の意味のエラーをgoogleなどの検索エンジンで検索すると、SQLインジェクションの脆弱性があるであろうサイトがリストされたことがありました。アダルトサイトが多く、たぶんライセンス払ってないで勝手に使ってるんだろうなぁと思った記憶があります。アダルトサイトの利用者情報が漏えいしてしまうのは非常に怖いことですよね。

さて、ここまで攻撃について説明してきましたが、いよいよ防ぐ方法です。
もっとも確実な方法はSQLインジェクション攻撃が起きないアプリケーションを作成することですが、これは最後に説明します。
まずはアプリケーションのコーディングでミスがあっても、なるべく被害が発生しないようにする方法から説明します。

まずはエラー内容を画面に出さないことです。このアプリケーションではエラーが発生したときに画面にエラー内容が出てきてしまっています。攻撃者はこのエラー内容を頼りに脆弱性を探りあてて攻撃してきます。今回はエラー内容を文字列として画面に表示していますが、エラーハンドリングが正しくできておらずWEBサーバーなどのデフォルトのエラーページ(利用しているWEBサーバーのバージョン情報などもでてしまうことがあります)が、エンドユーザーや攻撃者に表示されてしまうことも避けましょう。ちなみに検索エンジンにはWEBサーバーのデフォルトのエラーページがたくさんリストされていましたが、ある時にすべて検索結果からエラーページは削除されたようです。

攻撃者に対して、エラー内容を戻さないことで攻撃の糸口を攻撃者に見せないことも重要ですが、エラーが発生したこと自体はシステム運用者がきちんと把握して、被害がでるまえに攻撃の予兆を確認したり、アプリケーションを修正していくことも同様に重要です。
ただし、アプリケーション側でエラーハンドリングをきちんと作りこんでいる場合、アプリケーションのエラーログが残らないこともありますので、データベース側でもアプリケーションが発行するSQL文でエラーが起きていないかどうか監査し、定期的に確認することが重要です。攻撃者はトライアンドエラーでシステムの脆弱性を探してきます。それぞれのシステムは固有のものであり、情報収集をするうえで、データベースエラーが発生してしまうことは多いです。しかし、それを監査し、定期的に確認しておかないと、攻撃者には無限の時間があることになり、いつかは攻撃は成功してしまいます。ユーザーやアプリケーションが発行するSQL文でエラーが発生することは多くはないはずなので、攻撃の予兆を被害が出る前に発見するためにもデータベースの監査と監視をおこなってください。

さて、最後にそもそもSQLインジェクションを起こさないアプリケーションの作り方です。
変数はプレースホルダとしてあとから指定し、SQL構文を事前にコンパイルしておく、つまりPreparedStatementを利用します。PreparedStatementを利用することで、SQLインジェクション攻撃は防げますが、今回のアプリケーションのようにLIKE検査をおこなう場合には、ユーザーの入力値の前後に部分一致検索用の半角パーセント「%」をつける必要があるのと、ユーザーが入力した半角パーセント「%」と半角アンダーバー「_」は任意文字を表してしまうのでエスケープする必要があります。具体的には以下のように記載します。

PreparedStatement pstmt = conn.prepareStatement("select name from dbsecdemo.addrbook where name like ? escape '\\'");
pstmt.setString(1, "%" + inStr.replaceAll("\\\\","\\\\\\\\").replaceAll("%","\\\\%").replaceAll("_","\\\\_") + "%");
ResultSet rset = pstmt.executeQuery();

1行目ではSQL文のエスケープに半角バックスラッシュ「\」を利用することを宣言しています。ただし、「\」はJavaの文字列でもエスケープ文字として扱われるため2つ記載する必要があります。2行目でユーザーの入力値の前後に半角パーセント「%」をつけ、さらにユーザーの入力値内の半角パーセント「%」と半角アンダーバー「_」をエスケープします。replaceAllでそれぞれの文字をエスケープされた文字に置き換えますが、replaceAllでも「\」はエスケープ文字ですので、合計4つ「\\\\」と記載する必要があります。

(2017/6/22追記) 半角パーセント「%」と半角アンダーバー「_」のほかに半角バックスラッシュ「\」自身もエスケープする必要があります。エスケープしておかないと半角バックラッシュを含む文字列をユーザーが検索文字列に指定した際に「ORA-01424: エスケープ文字に続く文字がないか、または無効です。」エラーが発生することがあります。半角バックスラッシュをエスケープするため、上記のコードの中の紺いる文字の部分を追加しました。

このようにPreparedStatementを利用することで、半角シングルクォート「’」も文字列として扱われエラーは発生せず、またSQLインジェクション攻撃の文字列でも想定外のSQLが実行されることはありません。

次回もSQLインジェクション攻撃に関する話題の予定です。

「もくじ」にどもる