実戦形式で学ぶホワイトハッカーの技術

初心者向けのEthical Hacking講座です。

SQLインジェクション

では次にSQLインジェクションを見ていきましょう。

SQLインジェクションは、不正にデータベースへのクエリを実行する攻撃手法です。通常、Webアプリの入力フォームなどからの入力値を悪用して実行します。
ユーザーからの入力値をそのままSQLクエリに組み込んで実行するようなプログラムでは、悪意のあるユーザーがSQLコマンドを入力することで、意図しないクエリを実行させることが可能になります。
SQLインジェクションの結果、攻撃者はデータベースから機密情報を抜き取る、データを改ざんする、管理者権限を得るなどの悪用が危惧されます。

SQLについて知らない方は下記リンクが参考になります。
udemy.benesse.co.jp

まずは前回の「2乗計算プログラム」同様、本来の挙動を確認しましょう。
今回は「ログインプログラム」ということで、ユーザーネームとパスワードを入力してログインするプログラムの様です。
まずは適当なユーザーネームとパスワードを入力してみましょう。

login failed

すると上記のように「Login failed」というログインに失敗した旨のメッセージが出てきます。適当に試し続けて偶然当てることは難しそうですが、ここで登場するのがSQLインジェクションです。

ではまずこのログインプログラムがどのような処理を行っているのかを簡単に説明します。
サーバーには、ユーザーのユーザーネームとパスワードを格納しているデータベースがあります。
通常のアプリでは、新規登録やサインアップにてユーザーネームとパスワードを入力した際に、その情報がデータベースに保存されます。
そしてログイン画面でユーザーが入力した情報とデータベースに格納されている情報が合致すれば、ログインできるという仕組みです。

今回は下記の様なデータベース(テーブル)があると仮定します。

+----+---------------+--------------------+
| id | username      | password           |
+----+---------------+--------------------+
|  1 | test          | ItShouldBeCracked  |
+----+---------------+--------------------+


「user」というテーブルに上記のユーザー情報が格納されています。
そしてログインの際のSQL文は下記のようになります。(あえてSQLインジェクションに対して脆弱な書き方をしています。)

SELECT * FROM user WHERE username = "${username}" AND password = "${password}"

${}は、ユーザーがログインの際に入力した値です。
このSQL文は、「user」というテーブルの「username」コラムと「password」コラムの値に対して、ユーザーの入力したユーザーネームとパスワードで合致するものがあれば、そのユーザーの情報を返すというものです。
「AND」が使用されているので、当たり前ですがユーザーネームとパスワード両方が一致しなければいけません。
「SELECT」と「FROM」は直感的に分かりやすいと思いますが、「WHERE」は感覚的に分かりづらいかもしれませんが、要は条件を表します。

ではこのSQL文の何が脆弱なのでしょうか?
それはユーザーの入力値がそのままSQL文に入ってしまっているからです。
何が問題かというと、ユーザーがSQL文を上書きできてしまう所です。
例えばユーザーから下記の様なペイロードが送られてきたらどうでしょうか?

username=test" -- 

ユーザーネームとして上記のペイロードが送信された場合、最終的にどのようなSQL文になるか考えてみましょう。(パスワードは何を送信しても成立します。)

SELECT * FROM user WHERE username = "test" -- " AND password = "whatever"

MySQLでは「--」はコメントを表すため、「--」以降は無視することができます。(色付けされているため分かりやすいと思います。)
このSQL文でのWHERE句の条件は、ユーザーネームがtestであることだけです。
つまりパスワードを当てる必要がなくなったことを意味します。

ではこのやり方を実践してみましょう。
まずはこのWebアプリが、SQLインジェクションに脆弱であるという確証を得ましょう。
ユーザーネームに「"」を入れてみます。するとSQLのエラー文が返ってきます。
これは「"」がSQL文の一部として解釈され、その結果シンタックスが崩れたエラー文です。
実際の本番環境のアプリケーションでエラー文が返ってくることは稀ですが、エラー文が返ってくるとSQLインジェクションの疑いが高まります。

このままブラウザ上で入力していってもいいですが、ペイロードが長くなるにつれて見づらくなってきます。ですので、ここでBurp Suiteを使いましょう。
Burp Suite内のブラウザにて適当なユーザーネームとパスワードを入力してください。
その後「Proxy」タブの「HTTP history」タブを選択します。
そしてユーザーネームとパスワードを入力したPOSTリクエスト上で右クリックして「Send to Repeater」を選択してください。

Send to Repeater

そして「Proxy」タブから「Repeater」タブへと移動すると、先ほど送信したPOSTリクエストが見えるはずです。
「Repeater」ではリクエストの中身を変更して、何度もリクエストを送信したい場合に適しています。
今回であれば、様々なSQLインジェクションペイロードを送信して、レスポンスから挙動を確認します。
一度「Send」ボタンを押してみて、レスポンスが返ってくるか確認しましょう。

Repeater

レスポンスが返ってきて、下にスクロールすると「Login failed」の文字が見えます。
では先ほどのSQLインジェクションペイロードを送信してみましょう。

username=test" -- 

ログインに失敗してしまいます。
それもそのはずで、先ほど説明したように、このペイロードではユーザーネームが一致している必要があります。
ではユーザーネームをどうやって取得するのか、答えはどこかにあるので探してみてください。

では正解発表です。
「ログインプログラム」のページソースを見てみましょう。

page source

すると下記のコメントが残っていました。
「For those who forgot username, here is your username: admin」
これでユーザーネームがadminであることが分かりました。現実世界でも開発者がうっかり重要な情報や機能を公開してしまう事があります。
これはページソースだけを指しているわけではなく、サブディレクトリやサブドメインに開発途中の脆弱なプログラムが公開されていたり、GithubのCommitにAPIキーが残っていたりと様々な状況が考えられます。
開発者としては気を付ける必要があり、セキュリティエンジニアとしては見逃さないようにしなければいけません。
では先ほどのペイロードをadminに変更しましょう。
ちなみに、"--"の後には必ずスペースを空けるようにしてください。

username=admin" -- 

送信すると、下記画像の様にadminとしてログインに成功したことがわかります。

Login successful

このようにSQLインジェクションを悪用することで、パスワードが分かっていない状態でもログインすることができます。
しかしそれでもハードルはあります。結局ユーザーネームは当てなくてはいけないということです。
実は今回の状況では、ユーザーネームとパスワードも分かっていない状態でログインが可能です。

では先ほどのSQL文でもう一度考えてみましょう。

SELECT * FROM user WHERE username = "${username}" AND password = "${password}"

先ほどお伝えしたように、WHERE句では条件を指定することができます。
現状は「AND」があるため、ユーザーネームとパスワード両方を合致させる必要がありました。
しかしユーザーネームとしてコメントアウト(--)を送信することができるため、AND以降を考える必要はなさそうです。
ではこんなペイロードはどうでしょうか。

username=" OR 1=1 -- 

このペイロードが挿入されると、最終的なSQL文は以下になります。

SELECT * FROM user WHERE username = "" OR 1=1 -- " AND password = "whatever"

WHERE句部分を解説すると、ユーザーネームが空白または、1が1であれば、userテーブルからユーザー情報を取得するという意味です。
皆さん気づきましたか?
そうです、「1=1」は当然ながら必ず成立します。
そして「1=1」が成立する時点で、ユーザーネームが当たっていようがいまいが、必ずWHERE句は真(True)になります。
今回のケースでは、データベースの一番上に存在するadminユーザーの情報が取得できます。

では実際に試してみましょう。下記のペイロードを送信しましょう。

username=" OR 1=1 -- 
Login successful

ちなみに今回のテーブルには2人のユーザーの情報が格納されているため、下記のペイロードを送ると別ユーザーとしてログインできます。

username=" OR 1=1 ORDER BY id DESC-- 
Login as john

これはidを逆から並べたときに一番上にいるユーザーの情報を取得するというものです。

対策

対策方法は静的プレースホルダーを使用することです。
XSSと同様に、こちらの対策方法も基本的にライブラリやフレームワークレベルで提供されているので、簡単に実装できます。
静的プレースホルダーは、クエリ(SQL文)の準備段階で変数をバインドします。この手法は、パラメータの値(ユーザーが入力した値など)がクエリの一部として評価される前に、データベースエンジンによって型がチェックされるため非常に安全です。
JavaScriptでの実装例が以下になります。

var username = req.body.username;
var password = req.body.password;
const query = 'SELECT * FROM user WHERE username = ? AND password = ?';
connection.query(query, [username, password], (error, results, fields) => {
  if (error) throw error;
  // データの処理
});

上記のように?(プレースホルダー)を使用することで、SQLインジェクションを対策することができます。
ちなみに、今回はパスワードをそのまま平文でデータベースに保存していますが、本来であらばソルトを用いたハッシュ化を必ず行うべきです。
気になる方は下記記事を読んでみてください。
auth0.com

いかかでしょうか。SQLインジェクションは、アプリケーション上に存在すると、他人のアカウントでログインできてしまうというおそろしい脆弱性です。
SQLインジェクションはログイン機能だけに存在するわけではなく、サーバーにてSQL文を使用する処理になっていれば、あらゆる機能がSQLインジェクションの対象になります。
今回はSQLインジェクションでログインの不正を行いましたが、他にもデータベースの情報を不正に入手・削除したり、サーバーの侵入まで被害が及ぶことがあります。

次はOSコマンドインジェクションです。

learn-ethicalhacking.hatenablog.com