Skip to main content

手動 SQL Injection 漏洞利用

14 min3,995 words

SQL注入漏洞之背景介紹中我們介紹了 SQL 基本指令以及兩個主要的關聯式資料庫,接著我們可以來看如何識別 SQL 注入漏洞後並進行利用,SQL Injection vulnerability 經常被 sqlmap 這個自動化工具發現,或者在操作時誤用觸發到

但我想我們還是要完整的了解如何手動觸發漏洞的樣態,才能在工具無法辨識的情況下能夠自行確認

利用錯誤導向 Payloads 來識別 SQL Injection 漏洞

一樣使用之前我們提到的PHP程式片段來分析漏洞的模樣

<?php  
$uname = $_POST['uname'];  
$passwd =$_POST['password'];  

$sql_query = "SELECT * FROM users WHERE user_name= '$uname' AND password='$passwd'";  
$result = mysqli_query($con, $sql_query);  
?>

可以注意到 $uname 以及 $passwd 這兩個參數都來自於使用者輸入提供,這代表我們可以控制 $sql_query 變數來造出不同的 SQL 搜尋語法

在某些情況下,SQL Injection 可以繞過驗證機制,這也是我們在許多滲透測試時第一個想要驗證的,當我們在 $uname 變數之後利用結尾引號(’),以及新增一組 OR 1=1 描述,帶著註解符號(—)跟兩個反斜線(//),就可以直接將 SQL 功能打斷

要使用這種語法做註解的話,需要兩個連續的破折號,並且至少需要一個空白鍵,而反斜線延伸至兩個,一來是這樣可以在payload裡明顯看出差異,二來是有些網頁應用服務會針對空格做保護,讓我們的 payload 不會太容易被清洗掉

上面的描述組合起來的話就會看起來像底下這樣

secologies' OR 1=1 -- //

而當該語法賦予給 $sql_query 變數進到 SQL 搜尋則會變成下面的樣子,從 PHP 的應用服務中送進 MySQL 伺服器中

SELECT * FROM users WHERE user_name = 'secologies' OR 1=1 --

由於我們加上了 OR 語法,會使得後面的所有邏輯都是 true 的狀態,這讓 WHERE 條件會將資料庫裡第一個使用者的 id 吐出來,不管該紀錄是不是能顯示的

尤其沒有其他種確認的功能存在,我們可以直接繞過驗證邏輯來獲得管理員權限,我們展示一下實驗的過程,輸入一組帳號密碼進頁面欄位裡,直接按下送出

Testing-for-SQLi-Authentication-Bypass-1
1

由於offsec這個使用者帳號驗證身份是不合法的,所以我們收到無效的密碼錯誤通知

接著下一步,我們嘗試輸入任意的特殊字元進 Username 的欄位裡,來測試看看會跟後端 SQL 伺服器如何互動?例如我們輸入單一個引號後再按一次送出

Testing-for-SQLi-Authentication-Bypass-2
1

這次我們收到來自 SQL 語法的錯誤警告,這代表我們可以正常跟資料庫做互動

要注意的是,SQL Injection 攻擊通常是在網頁應用服務中,透過內部系統管道取得搜尋服務回傳的值,在我們的實驗裡,因為目標機器網頁服務有把 SQL debugging 功能開啟,所以我們才能在前端看得到回傳結果
但如果是在大部分商業級網頁應用服務的話,通常會將顯示錯誤資訊的功能都關閉起來,因為這通常是清單裡會重點檢查的安全缺陷,所以讀者可能還需要搭配其他方法來取得搜尋回應的結果

接著我們根據上面得到的線索,我們貼之前提到的驗證 payload 進 Username 欄位裡

SQLi-payload-inside-the-‘Username-field
1

送出後這次我們得到不一樣的結果了

Testing-for-SQLi-Authentication-Bypass-2-1
1

讚!這次我們就得到驗證成功的訊息了,代表上面那一組 payload 是有用的,則我們可以更近一步的利用這個基準點,產生錯誤導向的 payload 來嘗試枚舉整個資料庫,再次嘗試利用方才截斷 SQL 搜尋的語法,在之後注入任意想要執行的語法

' or 1=1 in (select @@version) -- //

我們想要強制 SQL 語法讓資料庫吐出錯誤訊息給我們,在上述的目標裡,我們想要收到 MySQL 的版本資訊,其中我們使用 in 這個運算子來比較布林數值 (1=1) 跟資料庫版本資訊(通常是一組整數)

而這兩種資料型態不同的情況下造成應用服務會指出錯誤的原因,此時,我們便可以知道資料庫的版本資訊了

Note

一般來說 MySQL 接受 version() 以及 @@version 這兩個指令

所以我們嘗試在目標服務中 Username 欄位注入這個 payload,來驗證我們的想法跟查看輸出結果

Testing-for-Error-based-payload
1

在實驗機器上我們檢查到 MySQL 版本是 (8.0.28),這個回應則代表我們可以利用管理員權限終端機來直接跟資料庫進行互動,在這個條件下,我們可以假設搜尋資料庫的次數沒有限制,所以一開始我們可以嘗試把 users 這個表單裡所有的使用者都輸出來看

' OR 1=1 in (SELECT * FROM users) -- //

當我們測試這個 payload 時卻出現

Testing-for-Error-based-payload-2
1

這意思是我們一次只能搜尋一個列(column)的資訊,所以我們調整一下,從 users 表單只搜尋 password 這個欄位試試

' or 1=1 in (SELECT password FROM users) -- //

這次的 payload 則讓我們得到一些被 MD5 hash 重組過的密碼錯誤資訊

Retrieving-Users-Hashes
1

這多多少少是有些幫助的內容,畢竟我們還是取得了所有使用者的密碼,但另一個問題是,我們不知道各個 hash 過的密碼對應到的是哪個使用者

而這一點,我們可以加上 WHERE 條件指定我們想要取得的使用者密碼,在滲透測試時我們最想要的就是 admin 帳號,所以我們一開始就可以針對這個使用者

' or 1=1 in (SELCT password FROM users WHERE username = 'admin') -- //

我們嘗試送出上面的 payload 後,這次就從錯誤資訊取得了 admin 以及對應的 hash password

Retrieving-Users-Hashes-2
1

超讚!以上就是我們從這些錯誤導向的 SQL 注入漏洞中取得了預期的 hash user 驗證資訊

UNION-based Payloads

在上一節當中我們測試了內部系統的 SQL Injection,讓應用服務吐出我們想知道的錯誤資訊,與此同時,我們也應該來測試一下 UNION 組成的 SQL 注入漏洞

UNION 關鍵字一樣可以利用前面提到的語法,還更可以執行額外的 SELECT 功能,並且將回傳資訊都放回同一個搜尋裡,這代表,我們有辦法結合兩個搜尋進一組 payload 裡

但為了使用 UNION SQL Injection 攻擊,我們首先需要滿足兩個條件:

  1. 注入的 UNION 搜尋只能根據原本的搜尋欄位數量進行查詢
  2. 兩個 payloads 之間查詢欄位的資料型態必須是一樣的

我們舉個例子,要滿足上述的條件我們針對網頁應用服務造出一組 SQL 查詢 payload

$query = "SELCT * from customers WHERE name LIKE '".$_POST["search_input"]."%'";

這個 payload 會將所有在 customers 表單裡的資訊抓出來,同時,利用 LIKE 關鍵字搜尋我們輸入的資訊進行任意的比較,%符號則讓我們的輸入可以是任意的字元或空內容都允許

我們測試一下這個 payload 來看 customers 表單裡有什麼東西

Loading-the-Customer-Search-Portal
1

在針對目標服務制定攻擊策略之前,我們需要先知道目標表單裡實際有多少的欄位,儘管目前實驗機器中只有四個欄位,我們不應該假設資料庫裡就只有這些,可能還有其他種欄位存在

而為了發現正確的欄位數量,我們其實可以用下面的 payload 來測試

' ORDER BY 1-- //

上述的 payload 排序了指定的欄位,這項條件在指定欄位不存在時會報錯,每一次都增加一個欄位值,最終我們可以發現資料庫吐給我們6個欄位的錯誤資訊,這表示實際的欄位總共有5個欄位

Finding-the-Exact-Number-of-Columns
1

現在我們知道了實際有五個欄位在 SQL 搜尋時可以指定,下一步我們就需要確認各個欄位要如何指定,透過下列的 payload 我們可以進行確認

%' UNION SELECT 'a1', 'a2', 'a3', 'a4', 'a5' -- //

而我們則可以得到下面的回傳資訊

Displaying-the-Exact-Number-of-Columns
1

得到這一步時我們可以先截圖,以方便後續做對應時可以參考,接著我們可以嘗試第一次的攻擊,把目前資料庫名稱、使用者以及 MySQL 版本資訊一併查出來

%' UNION SELECT database(), user(), @@version, null, null -- //

由於我們想要查詢 customers 表單裡所有的資料,我們用百分號加上一個單引號來關閉整個語法,接著我們針對注入語法使用 UNION SELECT 條件,將目前所有資料庫名稱、使用者資訊以及 MySQL 資料庫版本資訊放在第一、第二及第三行欄位裡吐出來,剩下兩個欄位填 null

Enumerating-the-Database
1

在注入這項 payload 後,我們可以發現使用者資訊跟資料庫版本資訊呈現在最後一行,但卻沒有顯示資料庫名稱,這有可能是因為第一欄位通常保留給 ID 資訊,通常只接受整數資料型態,意思是沒辦法接收我們用 SELECT database() 語法回傳字串格式的內容

尤其網頁應用服務帶有資料庫功能通常回傳會忽略第一欄位,因為 ID 資訊通常對於終端使用者來說是沒什麼用的資訊

所以在這種條件下,我們更新一下我們的語法,這挺容易的,只需要右移一位我們想要枚舉的資訊,同時這又不會造成欄位的缺失或錯誤

' UNION SELECT null, null, database(), user(), @@version -- //

在上一次我們已經驗證了輸出是可預期的,所以這次我們就不用加上百分號,再一次執行我們的 payload

Fixing-the-SQL-Query
1

這一次的實驗中,所有我們想知道的三個欄位資訊都出現了,包括我們知道了資料庫名稱

接著我們根據這個基礎進行擴充,來驗證同一個資料庫裡其他的表單,我們先枚舉目前資料庫裡的資訊構造,利用 information_schema.columns 這個表單來查詢

我們可以嘗試從現有資料庫裡的 information_schema.columns 表單查看裡面所有的欄位,我們將資訊放在第二、三、四欄位裡,第一跟第五個欄位就一樣填 null 讓它空著就行

' union select null, table_name, column_name, table_schema, null from information_schema.columns where table_schema=database() -- //

執行上述的 payload 後我們取得了以下的內容

Dumping-the-Current-Database-Tables-Structure
1

上面的輸出驗證了我們目前所有表單名稱、欄位名稱以及待在哪個資料庫裡,有趣的是,我們發現了一個新的表單叫做 users,該表單包含了四個欄位,其中更有一組叫做 password

讓我們來嘗試造一組新的查詢,來看看 users 表單裡所有的內容資訊

' UNION SELECT null, username, password, description, null FROM users -- //

上面的 payload 我們想要取得應用服務表單裡的 username、password 以及 description

Dumping-Users-Credentials
1

太棒了!在上面的流程中我們的 UNION-based payload 可以從任何的表單裡取得所有 usernames 以及 MD5 hash 過的密碼,甚至包含管理員權限的帳號密碼

這些 MD5 資訊內容都是從明文密碼 hash 過後儲存起來的,但其實網路上都有適當的工具可以逆向回來

盲 SQL Injection

前兩節我們都是使用內部系統的 SQL Injection payloads,意思是我們都是透過跟網頁應用服務裡的資料庫內容進行互動

但如果我們無法跟資料庫直接互動到時,取得代之,我們就必須使用這種盲 SQL Injection (Blind SQL Injection)攻擊的方法,尤其在資料庫不會回應給我們任何東西,我們的語法行為就必須改成布林或時序基底的邏輯了

舉例來說,一般的布林基底盲 SQL 注入攻擊會讓應用服務在資料庫回應 TRUE 或 FALSE 時有不一樣的表現,故此稱作布林 (Boolean)
不過我們還是可以在應用服務的內容裡檢查到相關的值

儘管布林基底的注入攻擊也許不像是盲 SQL 注入攻擊的變體,但其實輸出的結果是影響網頁應用服務的內容,而不是資料庫本身,所以我們還是分類在這個區間裡

時序基底盲 SQL Injection 攻擊會指示資料庫需要等多少時間後,再把搜尋結果回傳輸出,基於回應的時間,攻擊者有辦法區分出搜尋狀態是 TRUE 或者 FALSE

我們來展示一下實驗這兩個盲 SQL Injection 攻擊,首先我們登入一組使用者

Testing-for-Blind-SQLi
1

仔細觀察後,我們發現 URL 裡將使用者資訊當作輸入參數,我們目前登入這組 offsec 所以可以看到 user 參數帶的是 offsec

接著應用服務就會去搜尋所有跟 offsec 有關的資訊回來,包括 Username、Password Hash以及 Description 的值,而為了測試布林基底的SQL 注入攻擊,我們嘗試利用 URL 造出一組 payload

http://192.168.50.16/blindsqli.php?user=offsec' AND 1=1 -- //

由於 1=1 會永遠的 TRUE,應用服務將會回傳資料庫裡指定參數使用者的所有資訊,利用這個語法,我們可以枚舉整個資料庫裡的內容,包括其他的使用者資訊,甚至可以擴充這個 SQL 查詢語法,來驗證出其他表單的內容

我們使用時序基底的 SQL Injection payload 一樣可以取得相同的結果

http://192.168.50.16/blindsqli.php?user=offsec' AND IF (1=1, sleep(3),'false') -- //

在上述的 payload 裡,我們還加上了 if 判斷式,由於有 1=1 永恆 TRUE 的條件,所以如果使用者資訊存在的話就會回傳 TRUE,反之,如果該使用者不存在內容的話就回傳 FALSE

至於回傳時間會根據資料庫的架構、大小、版本來決定時間,在我們的實驗中,由於 offsec 這個帳號是存在的,大概三秒左右就會回傳資訊給我們了

當我們在處理盲 SQL Injection 攻擊時,我們應該要時時刻刻注意應用服務的行爲,不管是有效或錯誤的回應,在上述的實驗中,我們驗證的是一個我們自己構造的使用者,但如果讀者在滲透測試時,由於不清楚目標機器服務的效能如何,回應時間有可能都不一定

所以採用這種攻擊方法時有時非常耗時,所以我們會經常利用自動化工具,諸如像是 sqlmap 這個工具,在之後會跟各位探討,而本文就是想讓讀者理解到如果工具找不出來時,又或者入侵到目標機器上沒有相關工具時,我們可以採用哪些方法來測試該目標是否存在 SQL Injection 漏洞

Footnotes

  1. OffSec, “SQL Injection Attacks: Manual SQL Exploitation,” The Penetration Testing with Kali Linux (PEN-200). 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

INTO_OUTFILE 條件來寫入網頁伺服器裡的檔案 但要讓這個方法成功的話,目標資料庫系統上要允許底層使用者可以在想要的路徑下寫檔案 舉例來說,我們回憶先前在 MySQL 應用服務裡建構 [[手動 SQL Injection 漏洞利用]] unionbased-payloads 的方法,要擴充搜尋語法裡含寫入...

Referenced in this post