<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址:http://drops.wooyun.org/tips/7301

    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,設置值為1的話,會出現1后面跟payload

    2:表示將檢測的參數的值更換為一個整數,然后將payload添加在這個整數的后面。 如我們寫的參數是id=1,設置值為2的話,會出現[數字]后面跟payload

    3:表示將檢測的參數的值直接更換成我們的payload。 如我們寫的參數是id=1,設置值為3的話,會出現值1直接被替換成了我們的payload。
    最終在389行:

    #!python
    boundPayload = agent.prefixQuery(fstPayload, prefix, where, clause)
    boundPayload = agent.suffixQuery(boundPayload, comment, suffix, where)
    reqPayload = agent.payload(place, parameter, newValue=boundPayload, where=where)
    

    組合前綴、后綴、payload等,生成請求的reqPayload。
    這其中有個cleanupPayload()函數,其實就是將一些值進行隨機化。如下圖,例如kb.chars.start,kb.chars.stop,這兩個變量是在基于錯誤的注入時,隨機產生的字符串。

    enter image description here

    在398行:

    #!python
    for method, check in test.response.items():
        check = agent.cleanupPayload(check, origValue=value if place not in (PLACE.
    URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER) else None)      
        if method == PAYLOAD.METHOD.COMPARISON:     
            def genCmpPayload():
                sndPayload = agent.cleanupPayload(test.response.comparison, 
    origValue=value if place not in (PLACE.URI, PLACE.CUSTOM_POST, 
    PLACE.CUSTOM_HEADER) else None)
                boundPayload = agent.prefixQuery(sndPayload, prefix, where, clause)
                boundPayload = agent.suffixQuery(boundPayload, comment, suffix, 
    where)
                cmpPayload = agent.payload(place, parameter, 
    newValue=boundPayload, where=where)
                return cmpPayload
            kb.matchRatio = None
            kb.negativeLogic = (where == PAYLOAD.WHERE.NEGATIVE)
            Request.queryPage(genCmpPayload(), place, raise404=False)
            falsePage = threadData.lastComparisonPage or ""     
            trueResult = Request.queryPage(reqPayload, place, raise404=False)
            truePage = threadData.lastComparisonPage or ""      
            if trueResult:
                falseResult = Request.queryPage(genCmpPayload(), place, 
    raise404=False)
                if not falseResult:
                    infoMsg = "%s parameter '%s' seems to be '%s' injectable " % (
    paramType, parameter, title)
                    logger.info(infoMsg)
                    injectable = True
            if not injectable and not any((conf.string, conf.notString, conf.
    regexp)) and kb.pageStable:
                trueSet = set(extractTextTagContent(truePage))
                falseSet = set(extractTextTagContent(falsePage))
                candidates = filter(None, (_.strip() if _.strip() in (kb.
    pageTemplate or "") and _.strip() not in falsePage and _.strip() 
    not in threadData.lastComparisonHeaders else None for _ in (
    trueSet - falseSet)))
                if candidates:
                    conf.string = candidates[0]
                    infoMsg = "%s parameter '%s' seems to be '%s' injectable (with 
    --string=\"%s\")" % (paramType, parameter, title, repr(conf.
    string).lstrip('u').strip("'"))
                    logger.info(infoMsg)
                    injectable = True
        elif method == PAYLOAD.METHOD.GREP:
            try:
                page, headers = Request.queryPage(reqPayload, place, content=True, 
    raise404=False)
                output = extractRegexResult(check, page, re.DOTALL | re.
    IGNORECASE) \
                        or extractRegexResult(check, listToStrValue( \
                        [headers[key] for key in headers.keys() if key.lower() != 
    URI_HTTP_HEADER.lower()] \
                        if headers else None), re.DOTALL | re.IGNORECASE) \
                        or extractRegexResult(check, threadData.lastRedirectMsg[1] 
    \
                        if threadData.lastRedirectMsg and threadData.
    lastRedirectMsg[0] == \
                        threadData.lastRequestUID else None, re.DOTALL | re.
    IGNORECASE)
                if output:
                    result = output == "1"
                    if result:
                        infoMsg = "%s parameter '%s' is '%s' injectable " % (
    paramType, parameter, title)
                        logger.info(infoMsg)
                        injectable = True
            except SqlmapConnectionException, msg:
                debugMsg = "problem occurred most likely because the "
                debugMsg += "server hasn't recovered as expected from the "
                debugMsg += "error-based payload used ('%s')" % msg
                logger.debug(debugMsg)
        elif method == PAYLOAD.METHOD.TIME:
            trueResult = Request.queryPage(reqPayload, place, 
    timeBasedCompare=True, raise404=False)
            if trueResult:
                # Confirm test's results
                trueResult = Request.queryPage(reqPayload, place, 
    timeBasedCompare=True, raise404=False)
                if trueResult:
                    infoMsg = "%s parameter '%s' seems to be '%s' injectable " % (
    paramType, parameter, title)
                    logger.info(infoMsg)
                    injectable = True
        elif method == PAYLOAD.METHOD.UNION:
            configUnion(test.request.char, test.request.columns)
            if not Backend.getIdentifiedDbms():
                if kb.heuristicDbms is None:
                    warnMsg = "using unescaped version of the test "
                    warnMsg += "because of zero knowledge of the "
                    warnMsg += "back-end DBMS. You can try to "
                    warnMsg += "explicitly set it using option '--dbms'"
                    singleTimeWarnMessage(warnMsg)
                else:
                    Backend.forceDbms(kb.heuristicDbms)
            if unionExtended:
                infoMsg = "automatically extending ranges for UNION "
                infoMsg += "query injection technique tests as "
                infoMsg += "there is at least one other (potential) "
                infoMsg += "technique found"
                singleTimeLogMessage(infoMsg)
            reqPayload, vector = unionTest(comment, place, parameter, value, 
    prefix, suffix)
            if isinstance(reqPayload, basestring):
                infoMsg = "%s parameter '%s' is '%s' injectable" % (paramType, 
    parameter, title)
                logger.info(infoMsg)
                injectable = True
                # Overwrite 'where' because it can be set
                # by unionTest() directly
                where = vector[6]
        kb.previousMethod = method
    

    上面這部分代碼非常多,通過for循環遍歷payload中的標簽,遍歷的結果類似于

    enter image description here

    所以,上面的代碼可以分為:

    1.method為PAYLOAD.METHOD.COMPARISON:bool類型盲注 2.method為PAYLOAD.METHOD.GREP:基于錯誤的sql注入 3.mehtod為PAYLOAD.METHOD.TIME:基于時間的盲注 4.method為PAYLOAD.METHOD.UNION:union聯合查詢

    請注意,上面這四種方法,和之前說的六種注入方法不是一個概念,這里的是payload中的response代碼,而注入用的是request代碼。通過比較request的結果和response的結果,確定是否可以注入。以后的文章會介紹怎么比較的。。
    checkSqlInjectiond的關鍵部分就到這里了,后面就是把注入的數據保存起來。馬上會介紹讀取的時候。

    3.2.4 Payload生成條件

    前面具體介紹了Payload的生成方法,這里再總結一下條件:

    1.sqlmap會實現讀取payloads文件夾下xml文件中的每個test元素,然后循環遍歷。

    2.此時還會遍歷boundaries.xml文件。

    3.當且僅當某個boundary元素的where節點的值包含test元素where節點的值,clause節點的值包含test元素的clause節點的值,該boundary才能和當前的test匹配,從而進一步生成payload。

    4.where字段有三個值1:表示將我們的payload直接添加在值得后面[此處指的應該是檢測的參數的值] 如我們寫的參數是id=1,設置值為1的話,會出現1后面跟payload 2:表示將檢測的參數的值更換為一個整數,然后將payload添加在這個整數的后面。 如我們寫的參數是id=1,設置值為2的話,會出現[數字]后面跟payload 3:表示將檢測的參數的值直接更換成我們的payload。 如我們寫的參數是id=1,設置值為3的話,會出現值1直接被替換成了我們的payload

    5.最終的payload = url參數 + boundary.prefix+test.payload+boundary.suffix

    3.2.5 Action

    在start()的617行是action()函數,位于Action.py中,此函數是判斷用戶提供的參數,然后提供相應的函數。

    #!python
    if conf.getDbs:
        conf.dumper.dbs(conf.dbmsHandler.getDbs())
    if conf.getTables:
        conf.dumper.dbTables(conf.dbmsHandler.getTables())
    if conf.commonTables:
        conf.dumper.dbTables(tableExists(paths.COMMON_TABLES))
    

    3.2.6 HashDB

    sqlmap注入的結果會保存在輸出目錄的session.sqlite文件匯總,此文件是sqlite數據庫,可以使用SQLiteManager打開。
    回到controller.py中的start函數。第602行

    #!python
    _saveToResultsFile()
    _saveToHashDB()     
    _showInjections()   
    _selectInjection()  
    

    這四個函數的作用就是保存結果保存結果、保存session、顯示注入結果,包括類型,payload等。
    前面介紹過會判斷testSqlInj的值,如果為True,代表沒有測試過,會進入checkSqlInjection()函數,如果測試過,那么testSqlInj為false,就會跳過checkSqlInjection()。
    比如我們選擇--current-db時,通過action()進入到conf.dumper.currentDb(conf.dbmsHandler.getCurrentDb())。進入到databases.py的getCurrentDb中。

    #!python
    query = queries[Backend.getIdentifiedDbms()].current_db.query
    

    這是獲取相應的命令,比如mysql的命令是database().一直跟蹤函數到use.py的346行

    #!python
    if not value and not abortedFlag:
        output = _oneShotUnionUse(expression, unpack)
        value = parseUnionPage(output)
    

    _onehotUninoUse就是讀取session文件,獲取已經注入過的數據,如果session中沒有,代表沒有請求過,則重新請求獲取數據。output此時是獲取的網頁的源碼。

    #!python
    retVal = hashDBRetrieve("%s%s" % (conf.hexConvert, expression), checkConf=True)
    

    _onehotUninoUse的第一行,就是從session中獲取數據,跟蹤進hashdb.py的regrieve函數

    #!python
    def hashKey(key):
        key = key.encode(UNICODE_ENCODING) if isinstance(key, unicode) else repr(key)
        retVal = int(hashlib.md5(key).hexdigest()[:12], 16)     #注釋:hash的算法,對應數據庫中的id。md5后,轉換為10進制,就是session中的id
        return retVal
    def retrieve(self, key, unserialize=False):
        retVal = None
        if key and (self._write_cache or os.path.isfile(self.filepath)):
            hash_ = HashDB.hashKey(key)
            retVal = self._write_cache.get(hash_)
            if not retVal:      
                while True:
                    try:
                        for row in self.cursor.execute("SELECT value FROM storage WHERE id=?", (hash_,)):
                            retVal = row[0]
                    except sqlite3.OperationalError, ex:
                        if not "locked" in ex.message:
                            raise
                    except sqlite3.DatabaseError, ex:
                        errMsg = "error occurred while accessing session file '%s' ('%s'). " % (self.filepath, ex)
                        errMsg += "If the problem persists please rerun with `--flush-session`"
                        raise SqlmapDataException, errMsg
                    else:
                        break
        return retVal if not unserialize else unserializeObject(retVal)
    

    通過HashDB.hashKey()計算id,然后到session.sqlite中找記錄,那么key是怎么生成的呢?
    在common.py中有個hashDBRetrieve(),

    #!python
    def hashDBRetrieve(key, unserialize=False, checkConf=False):
        _ = "%s%s%s" % (conf.url or "%s%s" % (conf.hostname, conf.port), key, HASHDB_MILESTONE_VALUE)
        retVal = conf.hashDB.retrieve(_, unserialize) if kb.resumeValues and not (checkConf and any((conf.flushSession, conf.freshQueries))) else None
        if not kb.inferenceMode and not kb.fileReadMode and any(_ in (retVal or "") for _ in (PARTIAL_VALUE_MARKER, PARTIAL_HEX_VALUE_MARKER)):
            retVal = None
        return retVal
    

    此函數用于生成hash的key,生成方法為url+'None'+命令+HASHDB_MILESTONE_VALUE,比如u'http://127.0.0.1:80/biweb/archives/detail.phpNoneDATABASE()JHjrBugdDA'。此key經過int(hashlib.md5(key).hexdigest()[:12], 16),就是對應session中的id

    enter image description here

    最終在session.sqlite中根據id,就能夠找到記錄。

    enter image description here

    如上圖,獲取到的記錄其實就是一個網頁的源代碼,另外可以看到current-db的前后有幾個字符串,這個字符串就是kb.chars.start和kb.chars.stop
    回到_oneShotUnionUse中,如果session中沒有記錄,則會重新進行請求,獲取數據

    #!python
    vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector
            kb.unionDuplicates = vector[7]
            kb.forcePartialUnion = vector[8]
            query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, limited)    
            where = PAYLOAD.WHERE.NEGATIVE if conf.limitStart or conf.limitStop else vector[6]
            payload = agent.payload(newValue=query, where=where)
    

    最終的值通過解析session中的記錄value = parseUnionPage(output),找到kb.chars.start和kb.chars.stop中間的值,就是結果。

    0x04 結束


    還有很多東西沒有寫出來,希望后面的幾篇文章能夠寫好。花了好久的時間,調試、碼字,不知道又沒有人能看到最后。。

    97av