SQLMAP原始碼分析Part1:流程篇
0x00 概述
1.drops之前的文件 SQLMAP進階使用介紹過SQLMAP的高階使用方法,網上也有幾篇介紹過SQLMAP原始碼的文章曾是土木人,都寫的非常好,建議大家都看一下。
2.我準備分幾篇文章詳細的介紹下SQLMAP的原始碼,讓想了解的朋友們熟悉一下SQLMAP的原理和一些手工注入的語句,今天先開始第一篇:流程篇。
3.之前最好了解SQMAP各個選項的意思,可以參考sqlmap使用者手冊和SQLMAP目錄doc/README.pdf
4.內容中如有錯誤或者沒有寫清楚的地方,歡迎指正交流。有部分內容是參考上面介紹的幾篇文章的,在此一併說明,感謝他們。
0x01 流程圖
0x02 除錯方法
1.我用的IDE是PyCharm。
2.在選單欄Run->Edit Configurations。點選左側的“+”,選擇Python,Script中選擇sqlmap.py的路徑,Script parameters中填入注入時的命令,如下圖。
3.開啟sqlmap.py,開始函式是main函式,在main函式處下斷點。
4.右鍵Debug 'sqlmap',然後程式就自動跳到我們下斷點的main()函式處,後面可以繼續新增斷點進行除錯。如下圖,左邊紅色的代表跳轉到下一個斷點處,上面紅色的表示跳到下一句程式碼處
5.另外,如果要在程式碼中加中文註釋,需要在開始處新增以下語句:#coding:utf-8。
0x03 流程
3.1 初始化
我這裡用的版本是:1.0-dev-nongit-20150614
miin()函式開始73行:
#!python
paths.SQLMAP_ROOT_PATH = modulePath()
setPaths()
進入common.py中的setPaths()函式後,就可以看到這個函式是定義SQLMAP路徑和檔案的,類似於:
#!python
paths.SQLMAP_EXTRAS_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "extra")
paths.SQLMAP_PROCS_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "procs")
paths.SQLMAP_SHELL_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "shell")
paths.SQLMAP_TAMPER_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "tamper")
paths.SQLMAP_WAF_PATH = os.path.join(paths.SQLMAP_ROOT_PATH, "waf")
接下來的78行函式initOptions(cmdLineOptions),包含了三個函式,作用如流程圖所示,設定conf,KB,引數. conf會儲存使用者輸入的一些引數,比如url,埠
kb會儲存注入時的一些引數,其中有兩個是比較特殊的kb.chars.start和kb.chars.stop,這兩個是隨機字串,後面會有介紹。
#!python
_setConfAttributes()
_setKnowledgeBaseAttributes()
_mergeOptions(inputOptions, overrideOptions)
3.2 start
102行的start函式,算是檢測開始的地方.start()函式位於controller.py中。
#!python
if conf.direct:
initTargetEnv()
setupTargetEnv()
action()
return True
首先這四句,意思是,如果你使用-d選項,那麼sqlmap就會直接進入action()函式,連線資料庫,語句類似為:
#!python
python sqlmap.py -d "mysql://admin:[email protected]:3306/testdb" -f --banner --dbs --user
#!python
if conf.url and not any((conf.forms, conf.crawlDepth)):
kb.targets.add((conf.url, conf.method, conf.data, conf.cookie, None))
上面程式碼會把url,methos,data,cookie加入到kb.targets,這些引數就是我們輸入的
接下來從274行的for迴圈中,可以進入檢測環節
#!python
for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets:
此迴圈先初始化一些一些變數,然後判斷之前是否注入過,如果沒有注入過,testSqlInj=True,否則testSqlInj=false。後面會進行判斷是否檢測過。
#!python
def setupTargetEnv():
_createTargetDirs()
_setRequestParams()
_setHashDB()
_resumeHashDBValues()
_setResultsFile()
_setAuthCred()
372行setupTargetEnv()函式中包含了5個函式,這些函式作用是
1.建立輸出結果目錄
2.解析請求引數
3.設定session資訊,就是session.sqlite。
4.恢復session的資料,繼續掃描。
5.儲存掃描結果。
6.新增認證資訊
其中比較重要的就是session.sqlite,這個檔案在sqlmap的輸出目錄中,測試的結果都會儲存在這個檔案裡。
3.2.1 checkWaf
#!python
checkWaf()
if conf.identifyWaf:
identifyWaf()
377行checkWaf()是檢測是否有WAF,檢測方法是NMAP的http-waf-detect.nse,比如頁面為index.php?id=1,那現在新增一個隨機變數index.php?id=1&aaa=2,設定paoyload類似為AND 1=1 UNION ALL SELECT 1,2,3,table_name FROM information_schema.tables WHERE 2>1-- ../../../etc/passwd
,如果沒有WAF,頁面不會變化,如果有WAF,因為payload中有很多敏感字元,大多數時候頁面都會發生改變。
接下來的conf.identifyWaf代表sqlmap的引數--identify-waf,如果指定了此引數,就會進入identifyWaf()函式,主要檢測的waf都在sqlmap的waf目錄下。
當然檢測的方法都比較簡單,都是檢視返回的資料庫包種是否包含了某些特徵字元。如:
#!python
__product__ = "360 Web Application Firewall (360)"
def detect(get_page):
retval = False
for vector in WAF_ATTACK_VECTORS:
page, headers, code = get_page(get=vector)
retval = re.search(r"wangzhan\.360\.cn", headers.get("X-Powered-By-360wzb", ""), re.I) is not None
if retval:
break
return retval
if (len(kb.injections) == 0 or (len(kb.injections) == 1 and kb.injections[0].place is None)) \
and (kb.injection.place is None or kb.injection.parameter is None):
回到start函式,385行會判斷是否注入過,如果還沒有測試過引數是否可以注入,則進入if語句中。如果之前測試過,則不會進入此語句。
#!python
for place in parameters:
# Test User-Agent and Referer headers only if
# --level >= 3
skip = (place == PLACE.USER_AGENT and conf.level <
skip |= (place == PLACE.REFERER and conf.level < 3)
# Test Host header only if
# --level >= 5
skip |= (place == PLACE.HOST and conf.level < 5)
# Test Cookie header only if --level >= 2
skip |= (place == PLACE.COOKIE and conf.level < 2)
這中間sqlmap給了我們一些註釋,可以看到,level>=3時,會測試user-agent,referer,level>=5時,會測試HOST,level>=2時,會測試cookie。當然最終的測試判斷還要在相應的xml中指定,後面會介紹。
#!python
check = checkDynParam(place, parameter, value)
480行的checkDynParam()函式會判斷引數是否是動態的,比如index.php?id=1,透過更改id的值,如果引數是動態的,頁面會不同。
3.2.2 heuristicCheckSqlInjection
#!python
check = heuristicCheckSqlInjection(place, parameter)
502行有個heuristicCheckSqlInjection()函式,翻譯過來是啟發性sql注入測試,其實就是先進行一個簡單的測試,設定一個payload,然後解析請求結果。
heuristicCheckSqlInjection()在checks.py中,821行開始如下:
#!python
if conf.prefix or conf.suffix:
if conf.prefix:
prefix = conf.prefix
if conf.suffix:
suffix = conf.suffix
randStr = ""
while '\'' not in randStr:
randStr = randomStr(length=10, alphabet=HEURISTIC_CHECK_ALPHABET)
kb.heuristicMode = True
payload = "%s%s%s" % (prefix, randStr, suffix)
payload = agent.payload(place, parameter, newValue=payload)
page, _ = Request.queryPage(payload, place, content=True, raise404=False)
kb.heuristicMode = False
parseFilePaths(page)
result = wasLastResponseDBMSError()
首先conf.prefix和conf.suffix代表使用者指定的字首和字尾;在while '\'' not in randStr
中,隨機選擇'"', '\'', ')', '(', ',', '.'中的字元,選10個,並且單引號要在。接下來生成一個payload,類似u'name=PAYLOAD_DELIMITER\__1)."."."\'."__PAYLOAD_DELIMITER'。其中PAYLOAD_DELIMITER\__1和__PAYLOAD_DELIMITER是隨機字串。請求網頁後,呼叫parseFilePaths進行解析,檢視是否爆出絕對路徑,而wasLastResponseDBMSError是判斷response中是否包含了資料庫的報錯資訊。
#!python
value = "%s%s%s" % (randomStr(), DUMMY_XSS_CHECK_APPENDIX, randomStr())
payload = "%s%s%s" % (prefix, "'%s" % value, suffix)
payload = agent.payload(place, parameter, newValue=payload)
page, _ = Request.queryPage(payload, place, content=True, raise404=False)
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place
if value in (page or ""):
infoMsg = "heuristic (XSS) test shows that %s parameter " % paramType
infoMsg += "'%s' might be vulnerable to XSS attacks" % parameter
logger.info(infoMsg)
kb.heuristicMode = False
上面的程式碼是從888行開始,DUMMY_XSS_CHECK_APPENDIX = "<'\">",如果輸入的字串在頁面中返回了,會提示可能存在XSS漏洞。
接下來,我們回到start函式中,繼續看下面的程式碼。
#!python
if testSqlInj:
......
injection = checkSqlInjection(place, parameter, value)
在502行判斷testSqlInj,如果為true,就代表之前沒有檢測過,然後就會到checkSqlInjection,checkSqlInjection()才是真正開始測試的函式,傳入的引數是注入方法如GET,引數名,引數值。我們跟進。
3.2.3 checkSqlInjection
checkSqlInjection()在checks.py中,91行開始
#!python
paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place
tests = getSortedInjectionTests()
paramType是注入的型別,如GET。tests是要測試的列表,如下圖所示,包含了每個測試項的名稱,這些資料都是和/sqlmap/xml/payloads/目錄下每個xml相對應的。
#!python
if conf.dbms is None:
if not injection.dbms and PAYLOAD.TECHNIQUE.BOOLEAN in injection.data:
if not Backend.getIdentifiedDbms() and kb.heuristicDbms is False:
kb.heuristicDbms = heuristicCheckDbms(injection)
if kb.reduceTests is None and not conf.testFilter and (intersect(Backend.getErrorParsedDBMSes(), \
SUPPORTED_DBMS, True) or kb.heuristicDbms or injection.dbms):
msg = "it looks like the back-end DBMS is '%s'. " % (Format.getErrorParsedDBMSes() or kb.heuristicDbms or injection.dbms)
msg += "Do you want to skip test payloads specific for other DBMSes? [Y/n]"
kb.reduceTests = (Backend.getErrorParsedDBMSes() or [kb.heuristicDbms]) if readInput(msg, default='Y').upper() == 'Y' else []
if kb.extendTests is None and not conf.testFilter and (conf.level < 5 or conf.risk < 3) \
and (intersect(Backend.getErrorParsedDBMSes(), SUPPORTED_DBMS, True) or \
kb.heuristicDbms or injection.dbms):
msg = "for the remaining tests, do you want to include all tests "
msg += "for '%s' extending provided " % (Format.getErrorParsedDBMSes() or kb.heuristicDbms or injection.dbms)
msg += "level (%d)" % conf.level if conf.level < 5 else ""
msg += " and " if conf.level < 5 and conf.risk < 3 else ""
msg += "risk (%d)" % conf.risk if conf.risk < 3 else ""
msg += " values? [Y/n]" if conf.level < 5 and conf.risk < 3 else " value? [Y/n]"
kb.extendTests = (Backend.getErrorParsedDBMSes() or [kb.heuristicDbms]) if readInput(msg, default='Y').upper() == 'Y' else []
101行開始,這段程式碼主要是判斷DBMS型別,首先,如果使用者沒有手工指定dbms,則會根據頁面報錯或者bool型別的測試,找出DBMS型別,找出後,會提示是否跳過測試其他的DBMS。然後,對於測試出來的DBMS,是否用所有的payload來測試。
140行if stype == PAYLOAD.TECHNIQUE.UNION:會判斷是不是union注入,這個stype就是payload資料夾下面xml檔案中的stype,如果是union,就會進入,然後配置列的數量等,今天先介紹流程,union注入以後會介紹。
#!python
if conf.tech and isinstance(conf.tech, list) and stype not in conf.tech:
debugMsg = "skipping test '%s' because the user " % title
debugMsg += "specified to test only for "
debugMsg += "%s techniques" % " & ".join(map(lambda x: PAYLOAD.SQLINJECTION[x], conf.tech))
logger.debug(debugMsg)
continue
177行,就是使用者提供的--technique,共有六個選項BEUSTQ,但是現在很多文件,包括SQLMAP的官方文件都只給了BEUST的解釋說明,少個inline_query,相當於查詢語句中再加入一個查詢語句。
B: Boolean-based blind SQL injection(布林型注入)
E: Error-based SQL injection(報錯型注入)
U: UNION query SQL injection(可聯合查詢注入)
S: Stacked queries SQL injection(可多語句查詢注入)
T: Time-based blind SQL injection(基於時間延遲注入)
Q: inline_query(內聯查詢)
接下來,就是生成payload的過程。288行:
#!python
fstPayload = agent.cleanupPayload(test.request.payload, origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) else None)
test.request.payload為'AND [RANDNUM]=[RANDNUM]'(相應payload.xml中的request值)。根據此程式碼,生成一個隨機字串,如fstPayload=u'AND 2876=2876'。
302行:
#!python
for boundary in boundaries:
injectable = False
if boundary.level > conf.level and not (kb.extendTests and intersect(payloadDbms, kb.extendTests, True)):
continue
迴圈遍歷boundaries.xml中的boundary節點,如果boundary的level大於使用者提供的level,則跳過,不檢測。
307行:
#!python
clauseMatch = False
for clauseTest in test.clause:
if clauseTest in boundary.clause:
clauseMatch = True
break
if test.clause != [0] and boundary.clause != [0] and not clauseMatch:
continue
whereMatch = False
for where in test.where:
if where in boundary.where:
whereMatch = True
break
if not whereMatch:
continue
首先,迴圈遍歷test.clause(payload中的clause值),如果clauseTest在boundary的clause中,則設定clauseMatch = True,代表此條boundary可以使用。 接下來迴圈匹配where(payload中的where值),如果存在這樣的where,設定whereMatch = True。如果clause和where中的一個沒有匹配成功,都會結束迴圈,進入下一個payload的測試。
#!python
prefix = boundary.prefix if boundary.prefix else ""
suffix = boundary.suffix if boundary.suffix else ""
ptype = boundary.ptype
prefix = conf.prefix if conf.prefix is not None else prefix
suffix = conf.suffix if conf.suffix is not None else suffix
comment = None if conf.suffix is not None else comment
上面是設定payload的字首和字尾,如果使用者設定了,則使用使用者設定的,如果沒有,則使用boundary中的。
352行:
#!python
for where in test.where:
if where == PAYLOAD.WHERE.ORIGINAL or conf.prefix:
......
elif where == PAYLOAD.WHERE.NEGATIVE:
......
elif where == PAYLOAD.WHERE.REPLACE:
......
這裡的where是payload中的where值,共有三個值,where欄位我理解的意思是,以什麼樣的方式將我們的payload新增進去。
1:表示將我們的payload直接新增在值得後面[此處指的應該是檢測的引數的值] 如我們寫的引數是id=1,設定
相關文章
- SQLMap的前世今生(Part1)2020-08-19SQL
- sqlmap原始碼通讀(一)2019-07-18SQL原始碼
- sqlmap原始碼通讀(二)2019-07-18SQL原始碼
- BlogEngine.Net架構與原始碼分析系列part1:開篇介紹2010-04-13架構原始碼
- WindowManager呼叫流程原始碼分析2018-03-30原始碼
- 執行流程原始碼分析2024-09-27原始碼
- ReactNative原始碼篇:啟動流程2017-09-28React原始碼
- Flutter啟動流程原始碼分析2020-04-01Flutter原始碼
- View繪製流程原始碼分析2019-04-19View原始碼
- 原始碼分析Retrofit請求流程2018-11-04原始碼
- Mybatis執行流程原始碼分析2020-12-15MyBatis原始碼
- apiserver原始碼分析——啟動流程2021-10-04APIServer原始碼
- Activity啟動流程原始碼分析2018-03-29原始碼
- Spark job分配流程原始碼分析2015-10-13Spark原始碼
- Redux原始碼分析–Reducer篇2019-02-26Redux原始碼
- Redux原始碼分析--bindActionCreators篇2018-07-15Redux原始碼
- RxJava2原始碼分析(一):基本流程分析2019-01-06RxJava原始碼
- OkHttp 原始碼分析(一)—— 請求流程2019-03-12HTTP原始碼
- SpringMVC請求流程原始碼分析2022-05-29SpringMVC原始碼
- nacos註冊中心原始碼流程分析2020-12-23原始碼
- Tomcat原始碼分析--啟動流程2020-10-19Tomcat原始碼
- axios原始碼分析——請求流程2018-06-16iOS原始碼
- Laravel 5.4 生命流程與原始碼分析2017-12-17Laravel原始碼
- JobTracker啟動流程原始碼級分析2014-05-08原始碼
- Mapreduce原始碼分析分片、處理流程2014-08-12原始碼
- Net6 Configuration & Options 原始碼分析 Part12022-03-17原始碼
- 以太坊原始碼分析(39)geth啟動流程分析2018-05-14原始碼
- Android Activity啟動流程原始碼分析2019-02-28Android原始碼
- Spring IOC容器核心流程原始碼分析2021-08-16Spring原始碼
- SpringMVC執行流程及原始碼分析2021-03-06SpringMVC原始碼
- 微信MMKV原始碼分析(一) | 整體流程2018-09-27原始碼
- RecyclerView 原始碼分析(一) —— 繪製流程解析2021-01-17View原始碼
- Android原始碼分析:Activity啟動流程2018-03-29Android原始碼
- vue-router原始碼分析-整體流程2016-12-01Vue原始碼
- Netty啟動流程及原始碼分析2024-06-26Netty原始碼
- RocketMq 拉取資料流程原始碼分析2024-08-01MQ原始碼
- 深入OKHttp原始碼分析(一)----同步和非同步請求流程和原始碼分析2018-04-12HTTP原始碼非同步
- Pytorch版Faster R-CNN 原始碼分析+方法流程詳解——訓練篇2020-11-05PyTorchASTCNN原始碼