SQLMap 源碼閱讀

0x01 前言

因爲代碼功底太差,所以想嘗試閱讀 sqlmap 源碼一下,並且自己用 golang 重構,到後面會進行 ysoserial 的改寫;以及 xray 的重構,當然那個應該會很多參考 cel-go 項目。

0x02 環境準備

sqlmap 的項目地址:https://github.com/sqlmapproject/sqlmap用 pycharm 打斷點調試,因爲 vscode 用來調試比較麻煩。

因爲要動調,所以需要一個 sql 注入的靶場,這裏直接選用的是 sql-labs,用 docker 起

docker pull acgpiano/sqli-labs
docker run -dt --name sqli-lab -p [PORT]:80 acgpiano/sqli-labs:latest

最後還需要重新配置一下數據庫,然後才能以 sqli-labs 爲靶場進行測試。

這裏也掛一下 sqlmap 對應的一些基礎操作 ———— https://www.cnblogs.com/hongfei/p/3872156.html

直接在 pycharm 的 Debug 下進行調試,設置參數如下,開始調試

-u "http://81.68.120.14:3333/Less-1/?id=1" -technique=E --dbs

0x03 sqlmap 源碼閱讀

在開始之前我們有必要確認一下 sqlmap 運行的流程圖,很重要!這樣有助於我們進一步分析源碼。

1. 初始化

在 sqlmap.py 的 main 函數下斷點,開始調試

在沒有對 URL 進行發包/探測的時候 sqlmap 會先對一些環境、依賴、變量來做一些初始化的處理

【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備註 “博客園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

往下,通過 cmdLineParser() 獲取參數,cmdLineParser() 通過 argparse 庫進行 CLI 的打印與獲取,類似的一個小項目我之前也有接觸過 https://github.com/Drun1baby/EasyScan

往下 initOptions(cmdLineOptions) 解析命令行參數

init 函數: 初始化

在 init() 函數中通過調用各種函數進行參數的設置、payload 的加載等,有興趣的師傅可以點進去閱讀一下。

其中這三個相對比較重要,是用來加載 payload 的 ———— loadBoundaries()loadPayloads()_loadQueries()

loadBoundaries()  // 加載閉合符集合
loadPayloads()    // 加載 payload 集合
_loadQueries()    // 加載查詢語句,在檢測到注入點之後後續進行數據庫庫名字段名爆破會用到的語句

下個斷先點調試一下 loadBoundaries() 函數

首先,會去加載 paths.BOUNDARIES_XML,也就是 data/xml/boundaries.xml

接着進入解析 XML 文件的部分,跟進 parseXmlNode(root)

最終添加到 conf 對象的 tests 屬性裏

  • loadPayloads() 函數與 _loadQueries() 函數大體上也是如此,都是做了解析 xml 文件的工作,再將內容保存到 conf 對象的 tests 屬性裏。像 loadPayloads() 函數,最後在 conf.tests 裏面可以很清晰的看到 payloads

此時我們還可以看一下 conf 是什麼

conf 屬性中主要存儲了一些目標的相關信息(hostname、path、請求參數等等)以及一些配置信息,init 加載的 payload、請求頭 header、cookie 等

init() 函數執行完畢後,就會來到 start() 函數進行項目的正式運行。

初始化功能點小結

簡單概括一下初始化部分的代碼做了什麼事

  • 獲取命令行參數並處理

  • 初始化全局變量 conf 以及 kb

  • 獲取並解析幾個 xml 文件,完成閉合工作、payloads 加載工作

  • 設置 HTTP 相關配置,如 HTTP Header,UA,Session 等

2. URL 處理

f8 下來,先到的是 threadData = getCurrentThreadData(),繼續往下走,到 result = f(*args, **kwargs) 代碼塊,跟進一下

代碼邏輯此時來到了 /lib/controller/controller.py 下,往下走,是不會進到 conf.direct 和 conf.hashFile 中的,會直接進入到 kb.targets.add() 的代碼邏輯裏面。

此處的 kb 變量的作用是共享一些對象,其實本質上是保存了注入時的一些參數。kb.targets 添加了我們輸入的參數,如圖

往下看,大體上是做了一些類似類似打印日誌、賦值、添加 HTTP Header 等工作,這一部分代碼我們就不看了,直接看最關鍵的這一部分代碼 parseTargetUrl()

跟進

一開始先進行了這一判斷

if re.search(r"://\[.+\]", conf.url) and not socket.has_ipv6

判斷 http:// 的開頭形式是否正確,以及 socket 是否爲 ipv6 協議,如果爲 ipv6 協議,那麼 sqlmap 並不支持。

接着判斷

if not re.search(r"^(http|ws)s?://", conf.url, re.I):

判斷是 http 開頭還是 https 開頭,又或者是否是 ws/wss 開頭,如果沒有這些開頭,則就從端口判斷,這裏我認爲或許可以加上 80 與 8080 端口。

繼續往下看,進行了 url 的拆分、host 的拆分,並將這些內容保存到 conf 裏面的對應屬性,後續也是一些基礎的判斷與賦值,這裏不再贅述。

總而言之是在對 URL 進行剖析與拆解,最後這些東西都是放到 conf 裏面的

3. 如果這個網站已經被注入過,生成注入檢測的payload

核心代碼在 controller.py 的第 434 行,需跟進;此處我們可以設置對 kb.injections 的變量監測。先跟進 setupTargetEnv() 函數

  • setupTargetEnv() 函數調用瞭如下圖所示的七個函數

我們跟進最主要的 _resumeHashDBValues() 函數,首先調用了 hashDBRetrieve() 函數,設置檢索

出來,到第 476 行,這一次又調用了 hashDBRetrieve() 函數,傳參是 HASHDB_KEYS.KB_INJECTIONS,意思就是以 KB_INJECTIONS 作爲 KEY 進行檢索。跟進發現函數先將需要注入的 URL 信息放到了 _這個變量中,並將基礎信息用 | 符號隔開。

跟進 retrieve() 函數,這個函數做了生成 payload 的工作,具體是怎麼生成的我們繼續往下看

第 95 行,這裏很重要,執行了 SQL 語句,並通過 Hash 加密,加密方式是 base64Pickle 序列化

最終反序列化解密 Payload,說實話這裏沒看懂是怎麼生成的,看上去僅僅是執行了一個 SQL 語句,後面看其他師傅的文章的時候並沒有把這一段單獨拉出來說,payloads 其實都放在 xml 當中。

接着再循環一次,生成一個 payload

在生成完所有 payload 之後會先對目標進行一次探測,如果 Connection refused 則返回 False

這裏生成的 payload 只是很基礎的一部分,並非是

4. WAF 檢測

解析完 URL 之後對目標進行探測,往下看,位置是 controller.py 的第 439 行,第 448 行有 checkWaf() 的函數,很明顯就是要做 WAF 檢測的功能。

先會判斷這一目標是否存在 WAF,如果存在 WAF 的話,會進行字符的相關 fuzz,當然此處建議對一個存在 WAF 的目標進行測試。值得注意的是,如果這個目標你已經探測過存在 waf,且已知 waf 歸屬廠商的情況下,就不會走到 payload 那一段代碼邏輯當中去,相關的業務代碼在 hashDBRetrieve() 下,此處不再展開,比較容易。

如果存在 WAF,則會生成用於 fuzz 的 payload,這個 payload 是基於這個 NMAP 的 http-waf-detect.nse ———— https://seclists.org/nmap-dev/2011/q2/att-1005/http-waf-detect.nse

設置 payload 類似於 "9283 AND 1=1 UNION ALL SELECT 1,NULL,'<script>alert("XSS")</script>',table_name FROM information_schema.tables WHERE 2>1--/**/; EXEC xp_cmdshell('cat ../../../etc/passwd')#",如果沒有 WAF,頁面不會變化,如果有 WAF,因爲 payload 中有很多敏感字符,大多數時候頁面都會發生改變。

接下來的 conf.identifyWaf 代表 sqlmap 的參數 --identify-waf,如果指定了此參數,就會進入 identifyWaf() 函數,主要檢測的 waf 都在 sqlmap 的 waf 目錄下。不過新版的 sqlmap 已經將這一參數的功能自動放到裏面了,無需再指定參數

  • 這裏的 payload 先經過處理後賦值給 value,再將 value 作爲參數傳入 queryPage() 請求中,跟進

在經過很長一段的數據處理與判斷代碼後,我們到第 1531 行,如圖,跟進;getPage() 函數的作用是獲取界面的一些信息,如 url,ua,host 等,通過輸出比對 payload,爲判斷 waf 類型提供信息。

  • 獲取基本信息

這些基礎信息最後都會保存在 response 系列的 message 當中

getPage() 函數中調用了 processResponse() 函數做響應結果的處理,跟進

往下看,到 401 行開始,後續的代碼進行了 Waf 的識別

跟進 identYwaf.non_blind_check(),是通過正則表達式來對頁面進行匹配,對應的規則在 thirdparty/identywaf/data.json 中

同時 sqlmap 不光通過規則庫來進行判斷,也會通過頁面相似度來判斷是否存在 waf/ips

如果相似度小於設定的 0.5 那麼就判定爲有 waf 攔截

WAF 注入總結

總結一下就是兩點,一種方法是通過正則匹配的檢測,另外一種方法是根據頁面相似度來檢測,我自己應該很難寫出來 waf 檢測的東西;屆時再做嘗試。

5. 注入檢測之啓發式注入

從 checkWaf() 函數裏面出來,先到第 457 行,檢測網站是否穩定(因爲有些網站一測試可能就炸了)對應此 info

[INFO] testing if the target URL content is stable

繼續往下走到第 471 行,會先判斷參數是否可以注入,這裏與命令的參數 —— --level 掛鉤

在前文環境準備的時候我們採用的方式是報錯注入,如果不這麼做,直接指定參數 --dbs,無法進入到啓發式注入裏面。我們接着看代碼,往下直到第 581 行,調用的 heuristicCheckSqlInjection() 函數,意思是啓發性注入。

  • 啓發式注入做了哪些工作

1、數據庫版本的識別2、絕對路徑獲取3、XSS 的測試

數據庫版本的識別

首先會從 HEURISTIC_CHECK_ALPHABET 中隨機抽取10個字符出現構造 Payload,當然裏面的都不是些普通的字符,而且些特殊字符,當我們進行 SQL 注入測試的時候會很習慣的在參數後面加個分號啊什麼的,又或者是其他一些特殊的字符,出現運氣好的話有可能會暴出數據的相關錯誤信息,而那個時候我們就可以根據所暴出的相關錯誤信息去猜測當前目標的數據庫是什麼。

並且最後生成的這個 payload 是能夠閉合的

實際找個網站測試,如圖,這就是報出的 SQL 數據庫錯誤

判斷在 lib/request/connect.py 的 1532 行

接着跟進 processResponse() 函數,這裏和 waf 對比用的同一種方式,不再詳細說明

其中 processResponse() 會調用到 ./lib/parse/html.py 中的 htmlParser() 函數,這一個函數就是根據不同的數據庫指紋去識別當前的數據庫究竟是什麼。

最終實現這一功能的其實是 HTMLHandler 這個類,errors.xml 文件內容如圖

這一配置文件的比較簡單,其實也就是一些對應數據庫的正則。sqlmap 在解析 errors.xml 的時候,然後根據 regexp 中的正則去匹配當前的頁面信息然後去確定當前的數據庫。這一步和 WAF 比對類似。

到此 sqlmap 就可以確定數據的版本了,從而選擇對應的測試 Payload,後續我們會看到這是根據莫索引將 payloads 排序,然後選取對應數據庫信息的 payloads 進行測試。減少 sqlmap 的掃描時間。

  • 最後這個 DBMS 探測對應的是這一段信息

獲取絕對路徑與 XSS 探測

相比指紋識別,獲取絕對路徑的功能模塊相對簡單,利用正則匹配尋找出絕對路徑。

XSS 的探測也比較簡單,這裏就不作代碼分析了

6. 注入檢測之正式注入

從啓發式注入裏面出來,到第 592 行,進行正式的注入檢測,跟進

到第 130 行,獲取所有的 payload,後續會根據數據庫的信息構建索引,將符合索引的 payload 拿去攻擊

往下走,先判斷有沒有做數據庫信息的獲取,如果有則跳過,如果沒有就先進行上一步的啓發式注入

接着根據通過報錯得到的數據庫信息建立索引,將對應最有效的 payload 拿出來。這些 payloads 會進行 while 循環

第 370 行,通過 cleanupPayload() 函數對 payload 進行處理,主要功能其實是做了 payload 的標籤替換

最後替換過的 payload 長這樣

"AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('qbpxq',(SELECT (ELT(9125=9125,1))),'qxkvq','x'))s), 8446744073709551610, 8446744073709551610)))"

在 sqlmap 中將payload 分爲了三部分,上面生成的 fstpayload 就是中間那部分

prefix + payload + suffix 

prefix 和 suffix 就是對應的,閉合前面的結合以及註釋後面的結構,這兩個屬性主要是從 boundary 中進行獲取的,boundary 就是前面加載的 boundaries.xml 配置文件,用來閉合的,所以這裏作爲了 prefix 和 suffix

最後的拼接

並分別對 prefix 和 suffix 進行 clean,然後進行組合,組合之後的 payload 就是 reqPayload,然後進行請求

  • 發出請求最終還是通過 request.queryPage() 來實現的

請求完畢的結果經過 queryPage() 函數來獲取界面,但是頁面結果是由 kb.chars.start 和 kb.chars.stop 包裹着的

當第一次的注入不成功的時候,會不斷變更 prefix,suffix,當 prefix 和 suffix 都變更完畢但還是無法注入時,纔會變更 payload,取出另一個 payload 出來,直至 injectable 變量爲 true,同時 output=1

並且 injectable=true

7. 爆數據庫等操作

經過上一步正式注入的判斷,得到的 injectable=true 參數,才能進行下一步的爆數據庫操作.

爆庫階段主要是先經過四個函數處理數據後,再調用 action() 函數,跟進。

這裏已爆庫爲例,先看 --dbs 參數有關的這一塊,核心函數是 getDbs()

先根據後臺數據庫信息,輸出日誌

第 133 行,queries 就是存放之前初始化 queries.xml 的變量

首先通過 count(schema_name) 來獲取數據庫的個數,然後再通過 limit num,1 來依次獲取數據庫名,從 queries 變量中獲取語句之後就會傳遞到 getValue 函數

跟進,前面做了一些基礎的設置和 payload 的處理與賦值,比如第 401 行的 cleanQuery() 函數,將語句轉換爲大寫,這裏我就不跟進了。直接看關鍵語句,第 451 行,errorUse() 函數

在 errorUse() 中首先通過正則將 payload 中的各個部分都進行了獲取 ,保存到了對應的 field 當中,最終經過一系列處理,取出了 payload 中的 schema_name

跳出 getFields() 函數,往下,將 expression 的值經過 replace 操作,賦值給了 countedExpression,最終得到的值是 'SELECT COUNT(schema_name) FROM INFORMATION_SCHEMA.SCHEMATA'

第 337 行,跟進 _oneShotErrorUse() 函數,在這一個函數中,sqlmap 對目標網站發包,使用的 payload 爲 countedExpression,目的是探測數據庫個數(count)

具體業務發包在這裏

最後將結果傳入 extractRegexResult() 函數中進行正則提取

多線程的方式進行注入,而 runThreads() 函數調用了 errorThread() 函數,最終的注入業務還是由 errorThread() 函數來完成的

跟進一下 _errorFields() 函數,將每一個表進行 while 循環操作,再通過 limitQuery() 函數設置最後的 Limit 語句

最後成功 --dbs

sqlmap 流程分析結束

0x04 小結

sqlmap 的流程分析需要非常重視這張圖,當感覺代碼看不下去的時候看一下這張圖可以事半功倍。

在審計開始之前也可以看一下 utils 文件夾下的 python 文件,總體來說流程並不難,看正則的時候其實挺喫力的。

更多靶場實驗練習、網安學習資料,請點擊這裏>>

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章