sqlmap 核心分析 III: 核心邏輯
作者: ofollow,noindex" target="_blank">@v1ll4n
安全研發工程師,現就職於長亭科技,喜歡喵喵
本文的內容可能是大家最期待的部分,但是可能並不推薦大家直接閱讀本篇文章,因為太多原理性和整體邏輯上的東西散見前兩篇文章,直接閱讀本文可能會有一些難以預料的困難。:)
0x00 前言
上一篇文章,我們介紹了頁面相似度演算法以及 sqlmap 對於頁面相似的判定規則,同樣也跟入了 sqlmap 的一些預處理核心函式。在接下來的部分中,我們會直接開始 sqlmap 的核心檢測邏輯的分析,主要涉及到以下方面:
- heuristicCheckSqlInjection 啟發式 SQL 注入檢測(包括簡單的 XSS FI 判斷)
- checkSqlInjection SQL 注入檢測
0x01 heuristicCheckSqlInjection
這個函式位於 controller.py 的 start() 函式中,同時我們在整體邏輯中也明確指明瞭這一個步驟:

這標紅的兩個步驟其實就是本篇文章主要需要分析的兩個部分,涉及到 sqlmap 檢測 sql 注入漏洞的核心邏輯。其中 heuristicCheckSqlInjection 是我們本節需要分析的問題。這個函式的執行位置如下:

再上圖程式碼中,2標號為其位置。
啟發式 sql 注入檢測整體邏輯
通過分析其原始碼,筆者先行整理出他的邏輯:

根據我們整理出的啟發式檢測流程圖,我們做如下補充說明。
- 進行啟發式 sql 注入檢測的前提條件是沒有開啟 nullConnection 並且頁面並不是 heavilyDynamic。關於這兩個屬性,我們在第二篇文章中都有相關介紹,對於 nullConnection 指的是一種不需要知道他的具體內容就可以知道整個內容大小的請求方法;heavilyDynamic 指的是,在不改變任何引數的情況下,請求兩次頁面,兩次頁面的相似度低於 0.98。
- 在實際的程式碼中,決定注入的結果報告的,主要在於兩個標識位,分別為:casting 與 result。筆者在下方做程式碼批註和說明:

3. casting 這個標識位主要取決於兩種情況:第一種在第一個請求就發現存在了特定的型別檢查的跡象;第二種是在請求小數情況的時候,發現小數被強行轉換為整數。通常對於這種問題,在不考慮 tamper 的情況下,一般很難檢測出或者繞過。
4. result 這個標識位取決於:如果檢測出 DBMS 錯誤,則會設定這個標識位為 True;如果出現了資料庫執行數值運算,也置為 True。
XSS 與 FI
實際上在啟發式 sql 注入檢測完畢之後,會執行其他的檢測:

- 檢測 XSS 的方法其實就是檢查 “<‘\”>”,是否出現在了結果中。作為擴充套件,我們可以在此檢查是否隨機字串還在頁面中,從而判斷是否存在 XSS 的跡象。
- 檢測 FI(檔案包含),就是檢測結果中是否包含了 include/require 等報錯資訊,這些資訊是通過特定正則表示式來匹配檢測的。
0x02 checkSqlInjection
這個函式可以說是 sqlmap 中最核心的函數了。在這個函式中,處理了 Payload 的各種細節和測試用例的各種細節。
大致執行步驟分為如下幾個大部分:
- 根據已知引數型別篩選 boundary
- 啟發式檢測資料庫型別 heuristicCheckDbms
- payload 預處理(UIO/">NION)
- 過濾與排除不合適的測試用例
- 對篩選出的邊界進行遍歷與 payload 整合
- payload 渲染
- 針對四種類型的注入分別進行 response 的響應和處理
- 得出結果,返回結果
下圖是筆者摺疊無關程式碼之後剩餘的最核心的迴圈和條件分支,我們發現他關於 injectable 的設定完全是通過 if method == PAYLOAD.METHOD.[COMPARISON/GREP/TIME/UNION] 這幾個條件分支去處理的,同時這些條件顯然是 sqlmap 針對不同的注入型別的 Payload 進行自己的結果處理邏輯餓和判斷邏輯。

資料庫型別檢測 heuristicCheckDbms
我們在本大節剛開始的時候,就已經說明了第二步是確定資料庫的型別,那麼資料庫型別來源於使用者設定或者自動檢測,當截止第二步之前還沒有辦法確定資料庫型別的時候,就會自動啟動 heuristicCheckDbms 這個函式,利用一些簡單的測試來確定資料庫型別。

其實這個步驟非常簡單,核心原理是利用簡單的布林盲注構造一個 (SELECT “[RANDSTR]” [FROM_DUMMY_TABLE.get(dbms)] )=”[RANDSTR1]” 和 (SELECT ‘[RANDSTR]’ [FROM_DUMMY_TABLE.get(dbms)] )='[RANDSTR1]’ 這兩個 Payload 的請求判斷。其中
FROM_DUMMY_TABLE = { DBMS.ORACLE: " FROM DUAL", DBMS.ACCESS: " FROM MSysAccessObjects", DBMS.FIREBIRD: " FROM RDB$DATABASE", DBMS.MAXDB: " FROM VERSIONS", DBMS.DB2: " FROM SYSIBM.SYSDUMMY1", DBMS.HSQLDB: " FROM INFORMATION_SCHEMA.SYSTEM_USERS", DBMS.INFORMIX: " FROM SYSMASTER:SYSDUAL" }
例如,檢查是否是 ORACLE 的時候,就會生成
(SELECT 'abc' FROM DUAL)='abc' (SELECT 'abc' FROM DUAL)='abcd'
這樣的兩個 Payload,如果確實存在正負關係(具體內容參見後續章節的布林盲注檢測),則表明資料庫就是 ORACLE。
當然資料庫型別檢測並不是必須的,因為 sqlmap 實際工作中,如果沒有指定 DBMS 則會按照當前測試 Payload 的對應的資料庫型別去設定。
實際上在各種 Payload 的執行過程中,會包含著一些資料庫的推斷資訊(<details>),如果 Payload 成功執行,這些資訊可以被順利推斷則資料庫型別就可以推斷出來。
測試資料模型與 Payload 介紹
在實際的程式碼中,checkSqlInjection 是一個接近七百行的函式。當然其行為也並不是僅僅通過我們上面列出的步驟就可以完全概括的,其中涉及到了很多關於 Payload 定義中欄位的操作。顯然,直到現在我們都並不是特別瞭解一個 Payload 中存在著什麼樣的定義,當然也不會懂得這些操作對於這些欄位到底有什麼具體的意義。所以我們沒有辦法在不瞭解真正 Payload 的時候開始之後的步驟。
因此在本節中,我們會詳細介紹關於具體測試 Payload 的資料模型,並且基於這些模型和原始碼分析 sqlmap 實際的行為,和 sql 注入原理的細節知識。 ·
<test> 通用模型
關於通用模型其實在 sqlmap 中有非常詳細的說明,位置在 xml/payloads/boolean_blind.xml中,我們把他們分隔開分別來講解具體欄位對應的程式碼的行為。
首先我們必須明白一個具體的 testcase 對應一個具體的 xml 元素是什麼樣子:
<test> <title></title> <stype></stype> <level></level> <risk></risk> <clause></clause> <where></where> <vector></vector> <request> <payload></payload> <comment></comment> <char></char> <columns></columns> </request> <response> <comparison></comparison> <grep></grep> <time></time> <union></union> </response> <details> <dbms></dbms> <dbms_version></dbms_version> <os></os> </details> </test>
關於上面的一個 <test> 標籤內的元素都是實際上包含的不只是一個 Payload 還包含
Sub-tag: <title> Title of the test. 測試的名稱,這些名稱就是我們實際在測試的時候輸出的日誌中的內容

上圖表示一個 <test> 中的 title 會被輸出作為除錯資訊。
除非必要的子標籤,筆者將會直接把標註寫在下面的程式碼塊中,
Sub-tag: <stype> SQL injection family type. 表示注入的型別。 Valid values: 1: Boolean-based blind SQL injection 2: Error-based queries SQL injection 3: Inline queries SQL injection 4: Stacked queries SQL injection 5: Time-based blind SQL injection 6: UNION query SQL injection Sub-tag: <level> From which level check for this test. 測試的級別 Valid values: 1: Always (<100 requests) 2: Try a bit harder (100-200 requests) 3: Good number of requests (200-500 requests) 4: Extensive test (500-1000 requests) 5: You have plenty of time (>1000 requests) Sub-tag: <risk> Likelihood of a payload to damage the data integrity.這個選項表明對目標資料庫的損壞程度,risk 最高三級,最高等級代表對資料庫可能會有危險的•操作,比如修改一些資料,插入一些資料甚至刪除一些資料。 Valid values: 1: Low risk 2: Medium risk 3: High risk Sub-tag: <clause> In which clause the payload can work. 這個欄位表明 <test> 對應的測試 Payload 適用於哪種型別的 SQL 語句。一般來說,很多語句並不一定非要特定 WHERE 位置的。 NOTE: for instance, there are some payload that do not have to be tested as soon as it has been identified whether or not the injection is within a WHERE clause condition. Valid values: 0: Always 1: WHERE / HAVING 2: GROUP BY 3: ORDER BY 4: LIMIT 5: OFFSET 6: TOP 7: Table name 8: Column name 9: Pre-WHERE (non-query) A comma separated list of these values is also possible.
在上面幾個子標籤中,我們經常見的就是 level/risk 一般來說,預設的 sqlmap 配置跑不出漏洞的時候,我們通常會啟動更高級別 (level=5/risk=3) 的配置項來啟動更多的 payload。
接下來我們再分析下面的標籤
Sub-tag: <where> Where to add our '<prefix> <payload><comment> <suffix>' string. Valid values: 1: Append the string to the parameter original value 2: Replace the parameter original value with a negative random integer value and append our string 3: Replace the parameter original value with our string Sub-tag: <vector> The payload that will be used to exploit the injection point. 這個標籤只是大致說明 Payload 長什麼樣子,其實實際請求的 Payload 或者變形之前的 Payload 可能並不是這個 Payload,以 request 子標籤中的 payload 為準。 Sub-tag: <request> What to inject for this test. 關於發起請求的設定與配置。在這些配置中,有一些是特有的,但是有一些是必須的,例如 payload 是肯定存在的,但是 comment 是不一定有的,char 和 columns 是隻有 UNION 才存在 Sub-tag: <payload> The payload to test for. 實際測試使用的 Payload Sub-tag: <comment> Comment to append to the payload, before the suffix. Sub-tag: <char> 只有 UNION 注入存在的欄位 Character to use to bruteforce number of columns in UNION query SQL injection tests. Sub-tag: <columns> 只有 UNION 注入存在的欄位 Range of columns to test for in UNION query SQL injection tests. Sub-tag: <response> How to identify if the injected payload succeeded. 由於 payload 的目的不一定是相同的,所以,實際上處理請求的方法也並不是相同的,具體的處理方法步驟,在我們後續的章節中有詳細的分析。 Sub-tag: <comparison> 針對布林盲注的特有欄位,表示對比和 request 中請求的結果。 Perform a request with this string as the payload and compare the response with the <payload> response. Apply the comparison algorithm. NOTE: useful to test for boolean-based blind SQL injections. Sub-tag: <grep> 針對報錯型注入的特有欄位,使用正則表示式去匹配結果。 Regular expression to grep for in the response body. NOTE: useful to test for error-based SQL injection. Sub-tag: <time> 針對時間盲注 Time in seconds to wait before the response is returned. NOTE: useful to test for time-based blind and stacked queries SQL injections. Sub-tag: <union> 處理 UNION •注入的辦法。 Calls unionTest() function. NOTE: useful to test for UNION query (inband) SQL injection. Sub-tag: <details> Which details can be infered if the payload succeed. 如果 response 標籤中的檢測結果成功了,可以推斷出什麼結論? Sub-tags: <dbms> What is the database management system (e.g. MySQL). Sub-tags: <dbms_version> What is the database management system version (e.g. 5.0.51). Sub-tags: <os> What is the database management system underlying operating system.
在初步瞭解了基本的 Payload 測試資料模型之後,我們接下來進行詳細的檢測邏輯的細節分析,因為篇幅的原因,我們暫且只針對布林盲注和時間盲注進行分析,
真正的 Payload
我們在前面的介紹中發現了幾個疑似 Payload 的欄位,但是遺憾的是,上面的每一個 Payload 都不是真正的 Payload。實際 sqlmap 在處理的過程中,只要是從 *.xml 中載入的 Payload,都是需要經過一些隨機化和預處理,這些預處理涉及到的概念如下:
- Boundary:需要為原始 Payload 的前後新增“邊界”。邊界是一個神奇的東西,主要取決於當前“拼接”的 SQL 語句的上下文,常見上下文:注入位置是一個“整形”;注入位置需要單引號/雙引號(‘/”)閉合邊界;注入位置在一個括號語句中。
- –tamper:Tamper 是 sqlmap 中最重要的概念之一,也是 Bypass 各種防火牆的有力的武器。在 sqlmap 中,Tamper 的處理位於我們上一篇文章中的 agent.queryPage() 中,具體位於其對 Payload 的處理。
- “Render”:當然這一個步驟在 sqlmap 中沒有明顯的概念進行對應,其主要是針對 Payload 中隨機化的標籤進行渲染和替換,例如:[INFERENCE] 這個標籤通常被替換成一個等式,這個等式用於判斷結果的正負`Positive/Negative`
[RANDSTR] 會被替換成隨機字串
[RANDNUM] 與 [RANDNUMn] •會被替換成不同的數字
[SLEEPTIME] 在時間盲注中會被替換為 SLEEP 的時間
所以,實際上從 *.xml 中加載出來的 Payload 需要經過上面的處理才能真的算是處理完成。這個 Payload 才會在 agent.queryPage 的日誌中輸出出來,也就是我們 sqlmap -v3 選項看到的最終 Payload。
在上面的介紹中,我們又提到了一個陌生的概念,Boundary,並且做了相對簡單的介紹,具體的 Boundary,我們在 {sqlmap_dir}/xml/boundaries.xml 中可以找到:
<boundary> <level></level> <clause></clause> <where></where> <ptype></ptype> <prefix></prefix> <suffix></suffix> </boundary>
在具體的定義中,我們發現沒見過的子標籤如下:
Sub-tag: <ptype> What is the parameter value type. 引數•型別(引數邊界上下文型別) Valid values: 1: Unescaped numeric 2: Single quoted string 3: LIKE single quoted string 4: Double quoted string 5: LIKE double quoted string Sub-tag: <prefix> A string to prepend to the payload. Sub-tag: <suffix> A string to append to the payload.
其實到現在 sqlmap 中 Payload 的結構我們就非常清楚了
<prefix> <payload><comment> <suffix>
其中 <prefix> <suffix> 來源於 boundaries.xml 中,而 <payload> <comment> 來源於本身 xml/payloads/*.xml 中的 <test> 中。在本節中都有非常詳細的描述了
針對布林盲注的檢測
在接下來的小節中,我們將會針對幾種注入進行詳細分析,我們的分析依據主要是 sqlmap 設定的 Payload 的資料模型和其本身的程式碼。本節先針對布林盲注進行一些詳細分析。
在分析之前,我們先看一個詳細的 Payload:
<test> <title>PostgreSQL OR boolean-based blind - WHERE or HAVING clause (CAST)</title> <stype>1</stype> <level>3</level> <risk>3</risk> <clause>1</clause> <where>2</where> <vector>OR (SELECT (CASE WHEN ([INFERENCE]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</vector> <request> <payload>OR (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</payload> </request> <response> <comparison>OR (SELECT (CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN NULL ELSE CAST('[RANDSTR]' AS NUMERIC) END)) IS NULL</comparison> </response> <details> <dbms>PostgreSQL</dbms> </details> </test>
根據上一節介紹的子標籤的特性,我們可以大致觀察這個 <test> 會至少傳送兩個 Payload:第一個為 request 標籤中的 payload 第二個為 response 標籤中的 comparison 中的 Payload。
當然我們很容易想到,針對布林盲注的檢測實際上只需要檢測 request.payload 和 response.comparison 這兩個請求,只要這兩個請求頁面不相同,就可以判定是存在問題的。可是事實真的如此嗎?結果當然並沒有這麼簡單。
我們首先定義 request.payload 中的的請求為正請求 Positive,對應 response.comparison中的請求為負請求 Negative,在 sqlmap 中原處理如下:

在程式碼批註中我們進行詳細的解釋,為了讓大家看得更清楚,我們把程式碼轉變為流程圖:

其中最容易被遺忘的可能並不是正負請求的對比,而是正請求與模版頁面的對比,負請求與錯誤請求的對比和錯誤請求與模版頁面的對比,因為廣泛存在一種情況是類似檔案包含模式的情況,不同的合理輸入的結果有大概率不相同,且每一次輸入的結果如果報錯都會跳轉到某一個預設頁面(存在預設引數),這種情況僅僅用正負請求來區分頁面不同是完全不夠用的,還需要各種情形與模版頁面的比較來確定。
針對 GREP 型(報錯注入)
針對報錯注入其實非常好識別,在報錯注入檢測的過程中,我們會發現他的 response 子標籤中,包含著是 grep 子標籤:
<test> <title>MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)</title> <stype>2</stype> <level>5</level> <risk>1</risk> <clause>1,2,3,9</clause> <where>1</where> <vector>AND JSON_KEYS((SELECT CONVERT((SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]')) USING utf8)))</vector> <request> <payload>AND JSON_KEYS((SELECT CONVERT((SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]')) USING utf8)))</payload> </request> <response> <grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep> </response> <details> <dbms>MySQL</dbms> <dbms_version>>= 5.7.8</dbms_version> </details> </test>
我們發現子標籤 grep 中是正則表示式,可以直接從整個請求中通過 grep 中的正則提取出對應的內容,如果成功提取出了對應內容,則說明該引數可以進行注入。
在具體程式碼中,其實非常直觀可以看到:

再 sqlmap 的實現中其實並不是僅僅檢查頁面內容就足夠的,除了頁面內容之外,檢查如下項:
- HTTP 的錯誤頁面
- Headers 中的內容
- 重定向資訊
針對 TIME 型(時間盲注,HeavilyQuery)
當然時間盲注我們可以很容易猜到應該怎麼處理:如果發出了請求導致延遲了 X 秒,並且響應延遲的時間是我們預期的時間,那麼就可以判定這個引數是一個時間注入點。
但是僅僅是這樣就可以了嘛?當然我們需要了解的是 sqlmap 如何設定這個 X 作為時間點(請看下面這個函式,位於 agent.queryPage 中):

我們發現,它裡面有一個數學概念: 標準差
簡單來說,標準差是一組數值自平均值分散開來的程度的一種測量觀念。一個較大的標準差,代表大部分的數值和其平均值之間差異較大;一個較小的標準差,代表這些數值較接近平均值。例如,兩組數的集合{0, 5, 9, 14}和{5, 6, 8, 9}其平均值都是7,但第二個集合具有較小的標準差。述“相差k個標準差”,即在 X̄ ± kS 的樣本(Sample)範圍內考量。標準差可以當作不確定性的一種測量。例如在物理科學中,做重複性測量時,測量數值集合的標準差代表這些測量的精確度。當要決定測量值是否符合預測值,測量值的標準差佔有決定性重要角色:如果測量平均值與預測值相差太遠(同時與標準差數值做比較),則認為測量值與預測值互相矛盾。這很容易理解,因為如果測量值都落在一定數值範圍之外,可以合理推論預測值是否正確。
根據註釋和批註中的解釋,我們發現我們需要設定一個最小 SLEEPTIME 應該至少大於 樣本內平均響應時間 + 7 * 樣本標準差,這樣就可以保證過濾掉 99.99% 的無延遲請求。
當然除了這一點,我們還發現
delta = threadData.lastQueryDuration - conf.timeSec if Backend.getIdentifiedDbms() in (DBMS.MYSQL,):# MySQL's SLEEP(X) lasts 0.05 seconds shorter on average delta += 0.05 return delta >= 0
這一段程式碼作為 mysql 的 Patch 存在 # MySQL’s SLEEP(X) lasts 0.05 seconds shorter on average。
如果我們要自己實現時間盲注的檢測的話,這一點也是必須注意和實現的。
針對 UNION 型(UNION Query)
UNION 注入可以說是 sqlmap 中最複雜的了,同時也是最經典的注入情形。
其實關於 UNION 注入的檢測,和我們一開始學習 SQL 注入的方法是一樣的,猜解列數,猜解輸出點在列中位置。實際在 sqlmap 中也是按照這個來進行漏洞檢測的,具體的測試方法位於:

跟入 unionTest() 中我們發現如下操作
def unionTest(comment, place, parameter, value, prefix, suffix): """ This method tests if the target URL is affected by an union SQL injection vulnerability. The test is done up to 3*50 times """ if conf.direct: return kb.technique = PAYLOAD.TECHNIQUE.UNION validPayload, vector = _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix) if validPayload: validPayload = agent.removePayloadDelimiters(validPayload) return validPayload, vector
最核心的邏輯位於 _unionTestByCharBruteforce 中,繼續跟入,我們發現其檢測的大致邏輯如下:

別急,我們一步一步來分析!
猜列數
我相信做過滲透測試的讀者基本對這個詞都非常非常熟悉,如果有疑問或者不清楚的請自行百度,筆者再次不再贅述關於 SQL 注入基本流程的部分。
為什麼要把一件這麼簡單的事情單獨拿出來說呢?當然這預示著 sqlmap 並不是非常簡單的在處理這一件事情,因為作為一個滲透測試人員,當然可以很容易靠肉眼分辨出很多事情,但是這些事情在計算機看來卻並不是那麼容易可以判斷的:
- 使用 ORDER BY 查詢,直接通過與模版頁面的比較來獲取列數。
- 當 ORDER BY 失效的時候,使用多次 UNION SELECT 不同列數,獲取多個 Ratio,通過區分 Ratio 來區分哪一個是正確的列數。
實際在使用的過程中,ORDER BY 的核心邏輯如下,關於其中頁面比較技術我們就不贅述了,不過值得一提的是 sqlmap 在猜列數的時候,使用的是二分法(筆者看了一下,二分法這部分這似乎是七年前的程式碼)。

除此之外呢,如果 ORDER BY 失效,將會計算至少五個(從 lowerCount 到 upperCount)Payload 為 UNION SELECT (NULL,) * [COUNT],的請求,這些請求的對應 RATIO(與模版頁面相似度)會彙總儲存在 ratios 中,同時 items 中儲存 列數 和 ratio 形成的 tuple,經過一系列的演算法,儘可能尋找出“與眾不同(正確猜到列數)”的頁面。具體的演算法與批註如下:

我們發現,上面程式碼表達的核心思想就是 利用與模版頁面比較的內容相似度尋找最最不同的那一個請求。
定位輸出點
假如一切順利,我們通過上面的步驟成功找到了列數,接下來就應該尋找輸出點,當然輸出點的尋找也是需要額外討論的。其實基本邏輯很容易對不對?我們只需要將 UNION SELECT NULL, NULL, NULL, NULL, … 中的各種 NULL 依次替換,然後在結果中尋找被我們插入的隨機的字串,就可以很容易定位到輸入出點的位置。實際上這一部分的確認邏輯是位於下圖中的函式的 _unionConfirm

其中主要的邏輯是一個叫 _unionPosition 的函式,在這個函式中,負責定位輸出點的位置,使用的基本方法就是我們在開頭提到方法,受限於篇幅,我們就不再展開敘述了。
0x03 結束語
其實按筆者原計劃,本系列文章並沒有結束,因為還有關於 sqlmap 中其他技術沒有介紹:“資料持久化”,“action() – Exploit 技術”,“常見漏洞利用分析(udf,反彈 shell 等)”。但是由於內容是在太過龐雜,筆者計劃暫且擱置一下,實際上現有的文章已經足夠把 sqlmap 的 SQL 注入檢測最核心的也是最有意義的自動化邏輯說清楚了,我想讀讀者讀完之後肯定會有自己的收穫。