utsuroi sqli labs 知識ベース 04 / 05 — Blind SQLi
Blind SQL Injection · Knowledge Base
04

Blind SQL Injection

When the response is silent, listen with one bit at a time

クエリ結果が画面に出ない、エラーも握りつぶされる、ログも見えない——「目隠し(Blind)」の状況でも、サーバの挙動の1ビットの差を観測すれば情報は取れます。文字列を1文字ずつ、True/False の連鎖から復元する技法。

1. そもそも Blind とは #what-is-blind

「結果が見えない」が「全く分からない」ではない。

Lab 01〜10 のような「クエリ結果がそのまま画面に表示される」状況は、攻撃者にとって楽な環境です。でも実戦では、こういうケースの方が多い:

こうした状況を Blind SQL Injection と呼びます。「目隠し」状態でも、攻撃者は何かしらの「観測可能な差」を見つけて1ビットずつ情報を抽出します。

2. 1ビットの観測対象を見つける #one-bit

攻撃者は信号を選び、それを増幅する。

Blind SQLi の戦略は「True と False で何が変わるか」を観測することに集約されます。観測対象になりうる「1ビットの信号」は4つ:

  1. 応答内容の差 — 条件付きレスポンス。Welcome 表示の有無など (Lab 11)
  2. エラーの有無 — 条件付きエラー。1/0 除算を仕込む (Lab 12)
  3. 応答時間の差 — 時間ベース。pg_sleep で意図的に遅延 (Lab 14〜15)
  4. 外部通信の有無 — OAST。DNS 問合せの発生 (Lab 16〜17、次章)

この他に、エラーメッセージ自体に値が含まれる可視エラー(error-based)もあります(Lab 13)。これは Blind というより「エラーから一発で値を取る」攻撃ですが、Lab 11/12 と同じ系列で扱います。

3. SUBSTRING で1文字ずつ取り出す #substring

「password の N 文字目は X か?」を 26〜95 回繰り返す。

SUBSTRING は文字列から指定位置の文字を切り出す関数です:

SUBSTRING('naomi9', 1, 1) → 'n'   -- 1文字目を1文字
SUBSTRING('naomi9', 3, 1) → 'o'   -- 3文字目を1文字
SUBSTRING('naomi9', 1, 3) → 'nao' -- 1文字目から3文字

SQL の標準では SUBSTRING(文字列, 開始位置, 長さ)。Oracle は SUBSTR。開始位置は1始まり(プログラミング言語の0始まりと違う)。

Blind SQLi での使い方:

AND SUBSTRING(password, 1, 1) = 'a'   -- 1文字目は a か?
AND SUBSTRING(password, 1, 1) = 'b'   -- 1文字目は b か?
... a〜z + 0〜9 + 記号 を試す
AND SUBSTRING(password, 1, 1) = 'n'   -- 1文字目は n か?  → True!
AND SUBSTRING(password, 2, 1) = 'a'   -- 2文字目は...

パスワード長が N、文字種が M なら、最悪 N×M 回のリクエストで全パスワードを抽出できます(二分探索を使えば N×log₂(M) 程度に削減可能)。

パスワード長を先に知るには LENGTH:

AND LENGTH(password) = 7   -- 7文字か? を二分探索で

4. スカラーサブクエリ #scalar-subquery

クエリの中に別のクエリを書く。

Blind SQLi で重要な発想が「サブクエリ」。SELECT 文を (...) で囲んで「1つの値」として扱える機能です。1行1カラムだけ返すサブクエリをスカラーサブクエリと呼びます。

SELECT * FROM tracked_visitors
WHERE tracking_id = 'usr_xxx'
  AND (SELECT password FROM users WHERE username='admin') = 'naomi9'
       └────────── スカラーサブクエリ ──────────┘

別テーブル users から admin のパスワードを取得し、それが 'naomi9' と等しいかを判定する条件式。サブクエリの結果が等しければ全体が True になり、結果として元のクエリ(tracking_id 検索)が成功します。

これで、目に見えるのは「tracking_id の検索結果」だけだとしても、サブクエリの結果が条件式の真偽に影響を与えるので、外側のクエリの挙動からサブクエリの結果(=パスワード)を逆算できます。

5. 条件付きレスポンス(Lab 11) #conditional-response

Welcome が出るか出ないか、それが信号。

Lab 11 の状況: tracking_id が一致すると "Welcome back!" が表示、しないと表示なし。クエリ結果は使われていないが、クエリで0行が返ったら Welcome を出さないという挙動から SQLi 脆弱性が観測できます。

-- 条件 True のとき、tracking_id 一致 → Welcome 表示
SELECT * FROM tracked_visitors
WHERE tracking_id = 'usr_xxx'
  AND (SELECT 'a' FROM users WHERE username='admin'
       AND SUBSTRING(password,1,1)='n') = 'a'--

パスワード1文字目が n なら、サブクエリは 'a' を返し、最後の = 'a' が True になり、tracking_id 検索が成功 → Welcome 表示。1文字目が n でなければサブクエリが0行を返し、条件全体が False になり Welcome なし。

この「Welcome の有無」を文字種すべてで試して、True になる文字を特定。これを文字位置ごとに繰り返してパスワード全文を復元します。

6. 条件付きエラー(Lab 12) #conditional-error

Welcome が常に出るとき、エラーを起こさせる。

Lab 12 では Welcome が常に表示されるので Lab 11 の手法は使えません。代わりに CASE WHEN条件 True のときだけエラーを発生させます:

CASE WHEN (条件) THEN to_char(1/0) ELSE 'a' END
                 ↑                ↑
        条件Trueなら除算エラー  条件Falseなら無害

Oracle で 1/0 を計算すると ORA-01476: divisor is equal to zero エラーが発生します。エラーが出ればそのリクエストは 500 エラーで返り、出なければ正常応答。エラー有無で1ビット観測可能。

-- 1文字目が k か?
AND (SELECT CASE WHEN (SUBSTRING(password,1,1)='k')
       THEN to_char(1/0) ELSE 'a' END
     FROM users WHERE username='admin') = 'a'

エラーが出たら 1文字目は k、出なければ k ではない、と判定できます。

7. 可視エラーで一発漏洩(Lab 13) #visible-error

エラーメッセージが本番に出るなら、それは贈り物。

エラーメッセージが画面に表示される設定の場合、PostgreSQL の CAST 型変換エラーは攻撃者にとって金鉱です:

CAST('naomi9' AS int)
→ ERROR: invalid input syntax for type integer: "naomi9"
                                                  ──────
                                                  値が含まれる!

エラーメッセージの中に変換しようとした値がそのまま含まれます。サブクエリで取得したパスワードを CAST にかければ、エラー文に1回で全文が現れる:

AND 1 = CAST(
       (SELECT password FROM users WHERE username='admin')
       AS int)--

Lab 11/12 のような1文字ずつの抽出が不要。1リクエストで全パスワード取得。本番で SQL エラーを画面に出すのは絶対避けるべき設定です。

8. 時間ベース攻撃(Lab 14〜15) #time-based

応答時間が信号になる。

クエリが非同期実行されて応答に反映されない、エラーも握りつぶされる、Welcome 切替もない——そういう完全に静かな状況でも、応答時間は観測できます。

各 DB のスリープ関数:

Lab 14: 脆弱性の存在確認

まずは「SQLi が刺さるかどうか」を時間で確認:

-- スタッククエリで pg_sleep を実行
'; SELECT pg_sleep(10)--

応答が10秒以上遅れたら SQLi 脆弱性確認。

Lab 15: 時間で1ビットを観測

CASE WHEN と組み合わせて、True なら遅延・False なら即応答:

CASE WHEN (SUBSTRING(password,1,1)='p')
  THEN pg_sleep(10) ELSE pg_sleep(0) END

応答が10秒遅れたら 1文字目は p、即応答ならそうではない。これを文字位置×文字種で繰り返してパスワード復元。応答ごとに10秒待つので大量のリクエストを送る攻撃には時間がかかるのが時間ベース攻撃の弱点。

9. 自動化ツール #automation

手動の総当たりは現実的ではない。

パスワード長 20 文字、英数記号で文字種 80、二分探索なしで全文抽出するなら 20×80=1600 リクエスト。これを手動で実行するのは現実的ではありません。攻撃者は専用ツールを使います:

つまり「Blind SQLi だから抽出に時間がかかって攻撃しにくい」という防御は意味を持ちません。脆弱性さえあれば、ツールが自動で全データを抜きます。

本サイトでは抽出を手で体験

Lab 11〜15 は学習目的で「ヒントから1〜2文字を手で抽出して、残りはパスワード検証フォームで答え合わせ」という設計。実戦では sqlmap 等が全文を1〜数分で抜く、と理解した上で進めてください。