SQLMAP原始碼分析Part1:流程篇

wyzsk發表於2020-08-19
作者: 3xpl0it · 2015/07/23 10:16

0x00 概述


1.drops之前的文件 SQLMAP進階使用介紹過SQLMAP的高階使用方法,網上也有幾篇介紹過SQLMAP原始碼的文章曾是土木人,都寫的非常好,建議大家都看一下。
2.我準備分幾篇文章詳細的介紹下SQLMAP的原始碼,讓想了解的朋友們熟悉一下SQLMAP的原理和一些手工注入的語句,今天先開始第一篇:流程篇。
3.之前最好了解SQMAP各個選項的意思,可以參考sqlmap使用者手冊和SQLMAP目錄doc/README.pdf
4.內容中如有錯誤或者沒有寫清楚的地方,歡迎指正交流。有部分內容是參考上面介紹的幾篇文章的,在此一併說明,感謝他們。

0x01 流程圖

enter image description here

0x02 除錯方法


1.我用的IDE是PyCharm。
2.在選單欄Run->Edit Configurations。點選左側的“+”,選擇Python,Script中選擇sqlmap.py的路徑,Script parameters中填入注入時的命令,如下圖。 enter image description here

3.開啟sqlmap.py,開始函式是main函式,在main函式處下斷點。 enter image description here

4.右鍵Debug 'sqlmap',然後程式就自動跳到我們下斷點的main()函式處,後面可以繼續新增斷點進行除錯。如下圖,左邊紅色的代表跳轉到下一個斷點處,上面紅色的表示跳到下一句程式碼處

enter image description here

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,這些引數就是我們輸入的

enter image description here

接下來從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目錄下。

enter image description here

當然檢測的方法都比較簡單,都是檢視返回的資料庫包種是否包含了某些特徵字元。如:

#!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漏洞。

enter image description here

接下來,我們回到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相對應的。

enter image description here

#!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來測試。

enter image description here

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,設定

相關文章