[Burp Scanner Issue] 003 二階 SQL 注入(Second-Order SQL Injection)完整解析:成因、風險與修補指南

SQL 注入是老生常談的漏洞,但「二階 SQL 注入」(Second-Order SQL Injection,有時也稱為 Stored SQL Injection)卻是許多開發團隊最容易漏掉的變種。它不在輸入的當下爆炸,而是安靜地潛伏在資料庫裡,等到另一個功能把這筆資料撈出來重新組 SQL 時,才悄悄引爆。

這種延遲觸發的特性,讓它在開發過程中特別難被察覺——前端驗證通過了、寫入時也沒報錯、日誌也看不出異常,直到某個看似無關的功能成為攻擊的出口。Burp Scanner 將此漏洞列為 High 等級,CWE 對應到 SQL 命令的特殊元素處理不當(CWE-89)。對任何有使用者帳號、個人設定或資料儲存功能的系統而言,這都是一個不能輕忽的攻擊面。


一、這是什麼漏洞?

白話說明

一般的 SQL 注入(First-Order SQL Injection)是:使用者輸入惡意字串 → 系統直接把這段字串拼進 SQL → 資料庫執行惡意指令。

二階 SQL 注入的流程則不一樣:

  1. 第一步(儲存):使用者提交的惡意資料,在這個階段可能被「正確地」轉義或驗證,然後寫進資料庫,表面上看起來安全。
  2. 第二步(觸發):應用程式日後執行某個功能,從資料庫把這筆資料讀出來,然後「再次組合進另一條 SQL 查詢」時,沒有再做一次安全處理,導致注入發生。

舉個實際例子:

使用者在註冊時把帳號設為 <code>admin'–</code>。系統在存入資料庫時有做跳脫(escape),寫進去的值是 <code>admin''–</code>,看起來安全。但是當使用者修改密碼時,系統執行了這樣的查詢:

-- 危險的動態 SQL 組合,username 直接從資料庫撈出後拼接
UPDATE users SET password = 'newpass' WHERE username = 'admin'--'

這時候,原本存在資料庫的值被讀出來、直接拼進 SQL,造成注入。<code>–</code> 讓後面的條件被註解掉,攻擊者可能成功修改所有帳號的密碼,而非只有自己的帳號。

與一般 SQL 注入的差異

特性 一階 SQL 注入 二階 SQL 注入
觸發時機 輸入當下 資料被二次使用時
偵測難度 相對容易 較難,需跨功能追蹤
單一輸入點掃描 通常有效 可能漏掉
儲存在資料庫

二、Burp Scanner 為什麼會報這個問題?

偵測邏輯

二階 SQL 注入的偵測本質上比一般 SQL 注入困難,因為注入點(輸入)與觸發點(SQL 執行)分屬不同的請求與功能流程。Burp Scanner 偵測此類問題時,主要採用主動掃描(Active Scan)搭配跨功能的探針策略:

  1. 探針植入:在可能被儲存的輸入欄位(例如帳號名稱、個人簡介、地址、備註欄)植入特殊探針字串,這些字串設計成能在資料被二次讀取後觸發可觀察的行為差異(例如時間延遲、錯誤回應、特定輸出)。
  2. 後續觸發觀察:Burp 接著爬取並觸發其他會讀取這些資料的功能(例如個人資料頁、後台搜尋、報表生成),觀察是否出現與注入相關的異常回應。
  3. 差異比對:透過比對基準行為與注入後行為的差異,判斷是否存在可利用的注入點。

主動 vs. 被動掃描

這個問題主要透過主動掃描才能偵測到。被動掃描因為不主動送出探針,通常無法識別二階注入。這也表示,若掃描範圍或爬蟲設定不完整,部分二階注入可能不會被掃描器標記出來。

重要提醒

Burp Scanner 回報此問題,不等於該漏洞可以被穩定利用。 掃描器偵測到的是「疑似行為差異」,實際的可利用性取決於:

  • 注入點的位置與上下文
  • 資料庫類型與版本
  • 應用程式的錯誤處理是否對外揭露資訊
  • 背後的查詢結構

所有掃描結果都需要人工驗證,才能確認是真實漏洞還是誤報(False Positive)。


三、可能造成的風險與影響

二階 SQL 注入的風險等級為 High,實際影響視資料庫權限與應用程式邏輯而定,常見的危害包括:

機密性(Confidentiality)

攻擊者可能透過注入的 <code>UNION SELECT</code> 或子查詢,讀取資料庫中的敏感資料,例如使用者密碼雜湊(hash)、電子郵件、個資、API 金鑰,甚至跨資料表讀取原本無權限存取的資訊。

完整性(Integrity)

惡意 SQL 可以修改資料庫內容,例如竄改帳號密碼、修改訂單金額、插入虛假記錄,或刪除關鍵業務資料。

身份驗證繞過與權限提升

如同前述的密碼修改範例,攻擊者可能利用二階注入,在不知道原始密碼的情況下修改管理員帳號密碼,進而取得高權限存取。

橫向移動與資料庫伺服器控制

在資料庫帳號具備高權限(例如 <code>db_owner</code> 或 <code>sa</code>)的情況下,攻擊者可能進一步執行 <code>xp_cmdshell</code>(MSSQL)或 <code>LOAD_FILE</code> / <code>INTO OUTFILE</code>(MySQL),達成遠端指令執行或檔案讀寫。

為什麼不能因為「難以利用」而忽略?

二階 SQL 注入的利用條件確實比一般注入稍多,但這不是輕忽的理由。攻擊者通常有充裕的時間與精力研究應用程式流程;一旦找到觸發路徑,影響可能非常嚴重,且整個攻擊過程難以從日誌中一眼識別,增加了事後調查的難度。


四、常見成因

以下幾個開發與部署錯誤,是二階 SQL 注入最常見的根源:

1. 「存進去有做跳脫,讀出來就信任」的錯誤假設
最典型的成因。開發者在寫入資料庫時有做 <code>escape()</code> 或 <code>htmlspecialchars()</code>,然後誤以為這筆資料「已經安全了」,在後續讀出並重新組 SQL 時,沒有再次套用參數化查詢,直接用字串拼接。

2. 不一致的查詢寫法
專案中部分函式使用了 Prepared Statement,但某些舊模組、工具函式或報表查詢仍然使用字串拼接(例如 <code>"SELECT * FROM orders WHERE user = '" + username + "'"</code>),形成安全漏洞。

3. 中介層或 ORM 框架的原始查詢(Raw Query)未妥善處理
使用 ORM(例如 Sequelize、SQLAlchemy、Hibernate)的專案,若為了效能或特殊需求使用了 Raw SQL 或 <code>query()</code> 方法,但仍以字串插值方式傳入變數,等同於放棄了 ORM 的注入防護。

4. 日誌、通知、報表等「次要功能」疏於審查
主流程(登入、搜尋)通常受到較嚴格的審查,但「寄送通知信用到使用者資料」、「後台報表撈取使用者名稱」等次要功能,常在 Code Review 時被跳過,成為二階注入的溫床。

5. 多語言或多模組系統的邊界問題
當一個系統由 Python 後端寫入、Node.js API 讀取,或有多個微服務共用資料庫時,不同模組對「資料信任程度」的假設不一,容易造成某個服務讀出資料後直接用於 SQL 而未防護。

6. 測試資料或管理帳號的特殊字元未清理
開發或測試時,工程師可能建立帳號名稱帶有特殊字元(例如 <code>test'admin</code>)的測試資料,這些資料留存到正式環境後,若有功能把帳號名稱組進 SQL,就可能成為觸發條件。


五、攻擊者可能怎麼利用?

攻擊思路

攻擊者利用二階 SQL 注入的流程,通常如下:

  1. 偵察應用程式流程:找出哪些功能會「儲存使用者可控的資料」,以及哪些功能後續會「讀取並處理這些資料」。
  2. 植入惡意 Payload:在儲存階段提交精心設計的字串,讓它看起來無害(通過基本驗證),但包含 SQL 特殊字元。
  3. 觸發二次查詢:操作應用程式執行讀取並使用該資料的功能,觸發 SQL 注入。
  4. 觀察回應差異:根據錯誤訊息、行為差異、時間延遲來確認注入成立,進而調整 payload 擴大攻擊。

常見測試 Payload 範例

以下 payload 適合在滲透測試授權範圍內使用,用於驗證是否存在二階 SQL 注入:

字串跳脫測試(Boolean-based)

' OR '1'='1
admin'--
' OR 1=1--

時間延遲測試(Time-based Blind,適合無回顯情境)

  • MySQL:
    '; SELECT SLEEP(5);--
  • MSSQL:
    '; WAITFOR DELAY '0:0:5';--
  • PostgreSQL:
    '; SELECT pg_sleep(5);--

實際測試步驟建議

  1. 找到可儲存使用者資料的功能,例如:帳號名稱、個人簡介、地址欄位、標籤名稱。
  2. 在該欄位輸入含有單引號或 SQL 關鍵字的測試字串(例如 <code>test'</code>)並儲存。
  3. 操作會讀取並使用該資料的功能(例如:修改密碼、查看個人頁面、搜尋自己的訂單)。
  4. 觀察是否出現資料庫錯誤訊息、異常回應、內容變化,或在時間延遲測試中出現明顯的回應時間差異。
  5. 若確認注入成立,嘗試 <code>UNION SELECT</code> 確認是否可讀取其他資料表(需在授權範圍內進行)。

六、開發與維運團隊該怎麼修補?

6.1 短期補救措施

  • 立即審查已知的「先儲存、後讀取再組 SQL」流程,重點包含:帳號設定、個人資料更新、通知/郵件產生、後台搜尋與報表。
  • 暫時強化輸入驗證:對使用者可設定的字串欄位加入嚴格的白名單驗證,拒絕含有 <code>'</code>、<code>"</code>、<code>–</code>、<code>;</code>、<code>/*</code>、<code>*/</code> 等 SQL 特殊字元的輸入(此為緩解,非根治)。
  • 關閉或限制錯誤訊息對外輸出,避免攻擊者從 SQL 錯誤訊息中取得資料庫結構資訊。

6.2 正確修補方式

核心原則:所有組入 SQL 的變數,無論資料來源為何(包含從資料庫讀出的值),一律使用參數化查詢(Parameterized Query / Prepared Statement)。

Python(使用 psycopg2 for PostgreSQL)

# ❌ 危險寫法:從資料庫讀出 username 後直接拼接
cursor.execute(f"UPDATE users SET password = '{new_password}' WHERE username = '{username}'")

# ✅ 安全寫法:使用參數化查詢
cursor.execute(
    "UPDATE users SET password = %s WHERE username = %s",
    (new_password, username)
)

Node.js(使用 mysql2)

// ❌ 危險寫法
const query = <code>UPDATE users SET password = '${newPassword}' WHERE username = '${username}'</code>;
connection.query(query);

// ✅ 安全寫法:使用 ? 佔位符
connection.query(
  'UPDATE users SET password = ? WHERE username = ?',
  [newPassword, username],
  (err, results) => { /* ... */ }
);

Node.js(使用 Sequelize ORM 的 Raw Query)

// ❌ 危險寫法:Raw SQL 字串插值
await sequelize.query(<code>SELECT * FROM orders WHERE user = '${username}'</code>);

// ✅ 安全寫法:使用 replacements
await sequelize.query(
  'SELECT * FROM orders WHERE user = :username',
  { replacements: { username: username } }
);

關鍵心態轉換:不要問「這個值是不是已經安全了?」,而要問「這個值有沒有透過參數化查詢傳入 SQL?」後者才是正確的判斷標準。

6.3 長期預防建議

  • 建立全專案的 SQL 查詢規範:禁止字串拼接組 SQL,列入 Code Review Checklist 與靜態分析規則(例如使用 Semgrep 的 SQL injection 規則集)。
  • ORM 框架層級的強制約束:若使用 ORM,盡量避免 Raw Query;若確實需要,建立內部 wrapper 強制傳入參數而非字串。
  • 資料庫帳號最小權限原則:應用程式使用的資料庫帳號,只給予執行業務邏輯所需的最低權限,避免 <code>DROP TABLE</code>、<code>xp_cmdshell</code> 等高危操作可被觸發。
  • 導入 SAST 工具於 CI/CD Pipeline:在每次 Pull Request 或 merge 時,自動掃描是否引入了字串拼接的 SQL 寫法。
  • 定期的安全測試:在每個 Sprint 或 Release 前,對有儲存使用者資料的功能進行手動安全驗證。

七、如何驗證是否修好?

Burp Re-scan

修補完成後,使用 Burp Suite 對相同的功能重新執行主動掃描。若先前標記的問題不再出現,可作為一項參考依據,但不應作為唯一的驗證方式。

手動驗證重點

  1. 在儲存欄位輸入測試 payload(例如 <code>admin'–</code> 或 <code>'; SELECT SLEEP(3);–</code>)並儲存。
  2. 手動操作會觸發二次查詢的功能,確認:
    • 沒有出現資料庫錯誤訊息
    • 沒有異常的回應延遲(排除時間延遲注入)
    • 系統行為符合預期,不影響其他帳號或資料
  3. 確認 payload 的內容被當作純資料處理,而非 SQL 指令的一部分。

程式碼層級驗證

  • Code Review:確認所有從資料庫讀出的值在組合 SQL 時,都有使用 Prepared Statement 或 Parameterized Query。
  • 搜尋 codebase 中的字串拼接模式:
    # 簡易 grep 範例,尋找可疑的 SQL 組合
    grep -rn "execute(f\"" .
    grep -rn "execute(\"SELECT.*+\|\"UPDATE.*+\|\"INSERT.*+" .
  • 使用 SAST 工具(例如 Semgrep、Bandit for Python、NodeJsScan)執行全局掃描。

日誌觀察

啟用資料庫層級的慢查詢日誌(Slow Query Log)與錯誤日誌,觀察修補後是否仍有異常 SQL 錯誤或超時查詢出現。

單元測試 / 整合測試

撰寫測試案例,模擬使用者輸入含有 SQL 特殊字元的資料,並驗證後續使用該資料的功能不會產生 SQL 錯誤,且行為正確。

# 範例:Python unittest
def test_username_with_sql_injection_payload(self):
    # 建立帶有惡意 payload 的帳號
    self.create_user(username="admin'--")
    # 呼叫觸發二次查詢的功能
    result = self.change_password(username="admin'--", new_password="safe_pass")
    # 確認只有目標帳號被修改,且沒有引發錯誤
    self.assertEqual(result['status'], 'success')
    self.assert_only_one_user_affected()

八、從 SSDLC 角度,應該在哪個階段攔下來?

Requirements(需求階段)

在需求分析階段,應明確定義「所有使用者輸入的資料,即使已儲存至資料庫,在後續重新組合 SQL 時仍必須透過參數化查詢處理」為安全需求。這條規範應納入開發規格,而非留到測試階段才發現。

Design(設計階段)

設計資料庫存取層(DAL, Data Access Layer)或 Repository Pattern 時,應強制規定所有查詢方法必須接受參數化輸入,禁止提供字串直接拼接的接口。資料模型(Model)設計時,也應考慮哪些欄位可能被讀出後重新用於查詢,對這類欄位進行特別標記或設計約束。

Implementation(開發階段)

這是最關鍵的防線。 開發者應遵循統一的 SQL 查詢規範,所有 DB 存取一律使用 Prepared Statement 或 ORM 的安全介面,並在 Code Review 時明確檢查這一點。可在 IDE 或 Linter 層級加入規則,提示字串拼接的 SQL 寫法。

Verification(驗證階段)

安全測試(DAST/SAST)應在這個階段執行。SAST 工具可掃描字串拼接的 SQL 寫法;DAST(例如 Burp Suite)的主動掃描可嘗試偵測二階注入行為。Penetration Testing 也應包含「跨功能二階注入測試」的場景。此階段是發現遺漏的最後機會。

Deployment(部署階段)

部署時應確認:資料庫連線帳號使用最小權限;生產環境關閉詳細的 SQL 錯誤回傳;確認環境設定不殘留開發期間的測試資料(可能含有惡意 payload)。

Operations(維運階段)

定期執行安全掃描,監控資料庫異常存取行為(例如 <code>UNION SELECT</code>、<code>SLEEP()</code>、<code>xp_cmdshell</code> 等出現在查詢日誌中)。建立漏洞回報與修補流程,確保二階注入一旦在生產環境被發現,能夠快速回應。

最容易被發現與預防的階段

階段 效果
Implementation ⭐⭐⭐ 最佳預防點,從根源阻止
Design ⭐⭐⭐ 架構層面強制防護
Verification ⭐⭐ 最常發現的階段(DAST/滲透測試)
Operations ⭐ 最晚發現,修補代價最高

九、這個漏洞常見的誤解

誤解一:「我寫入資料庫時有做跳脫,這筆資料就是安全的了」

跳脫(Escaping)是針對「目前這次操作的 SQL 上下文」所做的處理,一旦資料被寫入資料庫,跳脫的效果就結束了。下次把這筆資料讀出來並重新組 SQL 時,就是一次全新的信任邊界,必須再次套用安全的查詢方式。跳脫≠永久安全,參數化查詢才是正解。

誤解二:「我用 Stored Procedure 就不會有 SQL 注入」

儲存程序(Stored Procedure)本身不能保證安全。如果 Stored Procedure 內部使用了動態 SQL(例如 MSSQL 的 <code>EXEC(@sql)</code>),且 <code>@sql</code> 是由外部輸入拼接而成,注入一樣可以發生。同樣地,即使 Stored Procedure 內部是安全的,若呼叫它的方式是以字串拼接使用者資料作為參數,問題一樣存在。

誤解三:「Burp 掃到了但我手動測了沒問題,就當誤報跳過」

二階 SQL 注入的觸發路徑可能需要特定的使用者流程或資料條件,單次手動測試不一定能重現。建議根據 Burp 的標記,仔細追蹤「資料從哪裡存入」、「在哪些功能被讀出並重新用於 SQL」,再做針對性的驗證,而非直接標記為誤報。

誤解四:「我的系統有 WAF,注入的 payload 會被擋掉」

WAF(Web Application Firewall)是有效的防禦層之一,但不應作為唯一防線。針對二階注入,攻擊者可能將惡意 payload 分散、編碼,或利用 WAF 對「資料庫內部讀取操作」無能為力的特性,繞過防護。真正的修補是在程式碼層面使用參數化查詢,WAF 只是輔助。


十、重點整理

  • 二階 SQL 注入的核心風險在於惡意資料「先存後發」,在另一個功能被讀出並重新組 SQL 時才觸發,傳統的輸入點防護容易遺漏這類攻擊路徑。
  • 唯一可靠的根治方式是參數化查詢(Prepared Statement),且必須對所有組入 SQL 的值一律套用,不論資料來源是使用者輸入還是從資料庫讀出的值。
  • 跳脫(Escape)、Stored Procedure、WAF 均不能單獨作為防護手段,它們可以作為縱深防禦的一部分,但不能取代參數化查詢。
  • Burp Scanner 標記此問題後,需要人工驗證觸發路徑,追蹤資料的「儲存點」與「二次使用點」,才能確認是否為真實可利用的漏洞。
  • 在 SSDLC 中,最有效的預防在 Design 與 Implementation 階段;透過架構設計強制安全查詢模式、Code Review 明確檢查,比事後掃描補救的代價低得多。

十一、參考對照

項目 內容
Burp Scanner Issue Name SQL injection (second order)
Severity High
Type Index (Hex) <code>0x00100210</code>
Type Index (Dec) <code>1049104</code>
CWE CWE-89: Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’)
CWE-94: Improper Control of Generation of Code (‘Code Injection’)
CWE-116: Improper Encoding or Escaping of Output
CAPEC CAPEC-66: SQL Injection
參考資源 PortSwigger Web Security Academy: SQL injection