[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 注入的流程則不一樣:
- 第一步(儲存):使用者提交的惡意資料,在這個階段可能被「正確地」轉義或驗證,然後寫進資料庫,表面上看起來安全。
- 第二步(觸發):應用程式日後執行某個功能,從資料庫把這筆資料讀出來,然後「再次組合進另一條 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)搭配跨功能的探針策略:
- 探針植入:在可能被儲存的輸入欄位(例如帳號名稱、個人簡介、地址、備註欄)植入特殊探針字串,這些字串設計成能在資料被二次讀取後觸發可觀察的行為差異(例如時間延遲、錯誤回應、特定輸出)。
- 後續觸發觀察:Burp 接著爬取並觸發其他會讀取這些資料的功能(例如個人資料頁、後台搜尋、報表生成),觀察是否出現與注入相關的異常回應。
- 差異比對:透過比對基準行為與注入後行為的差異,判斷是否存在可利用的注入點。
主動 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 注入的流程,通常如下:
- 偵察應用程式流程:找出哪些功能會「儲存使用者可控的資料」,以及哪些功能後續會「讀取並處理這些資料」。
- 植入惡意 Payload:在儲存階段提交精心設計的字串,讓它看起來無害(通過基本驗證),但包含 SQL 特殊字元。
- 觸發二次查詢:操作應用程式執行讀取並使用該資料的功能,觸發 SQL 注入。
- 觀察回應差異:根據錯誤訊息、行為差異、時間延遲來確認注入成立,進而調整 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);--
實際測試步驟建議
- 找到可儲存使用者資料的功能,例如:帳號名稱、個人簡介、地址欄位、標籤名稱。
- 在該欄位輸入含有單引號或 SQL 關鍵字的測試字串(例如 <code>test'</code>)並儲存。
- 操作會讀取並使用該資料的功能(例如:修改密碼、查看個人頁面、搜尋自己的訂單)。
- 觀察是否出現資料庫錯誤訊息、異常回應、內容變化,或在時間延遲測試中出現明顯的回應時間差異。
- 若確認注入成立,嘗試 <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 對相同的功能重新執行主動掃描。若先前標記的問題不再出現,可作為一項參考依據,但不應作為唯一的驗證方式。
手動驗證重點
- 在儲存欄位輸入測試 payload(例如 <code>admin'–</code> 或 <code>'; SELECT SLEEP(3);–</code>)並儲存。
- 手動操作會觸發二次查詢的功能,確認:
- 沒有出現資料庫錯誤訊息
- 沒有異常的回應延遲(排除時間延遲注入)
- 系統行為符合預期,不影響其他帳號或資料
- 確認 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 |