<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    【技術分享】sqlmap源碼解讀(1)

    VSole2021-07-23 17:01:02

    介紹

    作為web滲透界的神器之一,無論是挖掘src或者滲透測試,不少的師傅們都離不開這個工具。他的強大也不只是簡單地自動化注入,后續文章我會逐漸帶大家熟悉這個工具的原理。其實網上已有大佬做了很多的分析,我將更細致更基礎地進行分析

    當然,一開始就直接拿最新版本分析是不妥的,目前該工具已經趨于完善,內置各種插件腳本,直接閱讀將會受到很大的影響,因此我找到一個比較老且穩定的版本

    初始化

    sqlmap全局變量如下

    # 路徑相關paths = advancedDict()# 配置相關conf = advancedDict()# 共享一些對象kb = advancedDict()# 臨時對象temp = advancedDict()# 每個DBMS用到的語句queries = {}# 日志logger = LOGGER
    

    全局變量使用的是自帶dict和它實現了的advancedDict類型,具體代碼并不是很復雜,初始化加入一個__initialised屬性。在執行__init__的self.__initialised = True及之前時都會調用__setattr__,執行到第一個if條件進入,做到了在初始化的時候進行一些屬性的賦值。后續以advancedDistObj.attr=value對advancedDictObj賦值時會直接走第2個和第3個條件。額,其實說這么多,sqlmap這樣做是為了區別賦值方式,全局變量中凡是使用到advancedDict類型的在后續使用中只有advancedDistObj.attr=value這樣的格式,而全局變量中的dict類型會使用dictObj[key]=value這樣的格式

    class advancedDict(dict):    ......    def __init__(self, indict=None, attribute=None):        ......        self.attribute = attribute        dict.__init__(self, indict)        self.__initialised = True    ......    def __setattr__(self, item, value):        if not self.__dict__.has_key('_advancedDict__initialised'):            return dict.__setattr__(self, item, value)        elif self.__dict__.has_key(item):            dict.__setattr__(self, item, value)        else:            self.__setitem__(item, value)
    

    main函數

    # 在全局變量path中初始化一些路徑相關(輸出目錄等)setPaths()# 打印banner信息banner()# 解析命令行輸入參數cmdLineOptions = cmdLineParser()# 初始化init(cmdLineOptions)if conf.start:    # 啟動    start()
    

    初始化部分代碼量不小,簡單概括如下:

    • 合并命令行的一些參數
    • 初始化日志相關
    • 初始化全局變量conf和kb
    • 過濾命令行參數的多于字符
    • 設置Cookie/Referer/UA頭
    • 設置請求方法默認為GET
    • 處理HTTP基礎認證頭
    • 處理HTTP代理相關
    • 是否已知DBMS
    • 如果用戶使用了谷歌語法這個功能進行處理
    • 初始化urllib2的opener
    • 嘗試更新sqlmap版本和mssql的xml
    • 解析query的xml

    mssql.xml:mssql的xml是一個類似數據庫的文件,保存了每個版本的mssql的指紋信息(為了方便具體版本的識別)

    <root>    <signatures release="2008">        <signature>            <version>                10.00.1750            version>            <servicepack>                0+Q956718            servicepack>        signature>    ......    signatures>root>
    

    queries.xml:保存了注入需要用到的一些SQL語句

    <dbms value="MySQL">        <cast query="CAST(%s AS CHAR(10000))"/>        <length query="LENGTH(%s)"/>        <isnull query="IFNULL(%s, ' ')"/>        <delimiter query=","/>        <limit query="LIMIT %d, %d"/>        <limitregexp query="\s+LIMIT\s+([\d]+)\s*\,\s*([\d]+)"/>        <limitgroupstart query="1"/>        <limitgroupstop query="2"/>        <limitstring query=" LIMIT "/>       ......
    

    準備工作

    根據輸入參數得到URL后做基本的校驗

    def initTargetEnv():    # 正則結合分割字符串方式拿到url的host,port等基本信息    parseTargetUrl()    # 如果是GET注入的方式直接分割字符串拿到請求參數    # 如果是POST或HTTP頭注入需要輸入參數存在data文件,解析得到具體參數    __setRequestParams()    # 處理恢復功能(如果程序中斷下次啟動用到)    __setOutputResume()
    

    檢測是否連接成功(并沒有采用requests而是使用原生urllib2)

    checkConnection()
    

    然后進行Cookie的封裝,向用戶詢問使用新Cookie或提供的輸入參數。如果沒有進行Cookie注入會進行所有可能參數的注入檢測,這也是核心的一部分

    檢測閉合符號

    值得一看的是檢測注入前先進行穩定性檢測,延時請求三次目標頁面,如果三次結果不一致認為是不穩定的

    firstResult = Request.queryPage()    time.sleep(0.5)
        secondResult = Request.queryPage()    time.sleep(0.5)
        thirdResult = Request.queryPage()
        condition  = firstResult == secondResult    condition &= secondResult == thirdResult
    

    檢測每個參數是否動態,如果該參數不是動態的,也就是改變它不會造成頁面改變,那么認為它不存在注入,將會檢測下一個參數是否動態。而動態檢測類似穩定性檢測,都是三次請求頁面對比結果


    # 構造隨機數    randInt = randomInt()    # 這個agent相當于是做了個字符串拼接    payload = agent.payload(place, parameter, value, str(randInt))    dynResult1 = Request.queryPage(payload, place)
        # 如果改變這個參數但返回頁面一致,認為它不是動態的    if kb.defaultResult == dynResult1:        return False
        logMsg = "confirming that %s parameter '%s' is dynamic" % (place, parameter)    logger.info(logMsg)
        payload = agent.payload(place, parameter, value, "'%s" % randomStr())    dynResult2 = Request.queryPage(payload, place)
        payload = agent.payload(place, parameter, value, "\"%s" % randomStr())    dynResult3 = Request.queryPage(payload, place)
        condition  = kb.defaultResult != dynResult2    condition |= kb.defaultResult != dynResult3
    

    檢測到可能存在注入的參數后,將會進行核心函數checkSqlInjection,檢測是否存在注入以及注入類型。注意這里的注入類型不是報錯注入盲注這樣的意思,而是檢測它的閉合符號,是id=0這樣的數字注入還是key=value這樣的字符串注入,而字符串注入又分為單雙引號。下文的parenthesis是處理括號問題,例如select * from table where id=((1));,默認范圍是0-4,即沒有括號或最多三個括號,一般不會有超過三個括號的情況

    注意到首先構造一個true的payload,如果返回結果和不包含payload的頁面相等,進入第一個if。這時候構造一個false的payload,將結果再次對比,如果false和true的結果不一致,可以初步確認存在注入

        payload = agent.payload(place, parameter, value, "%s%s AND %s%d=%d" % (value, ")" * parenthesis, "(" * parenthesis, randInt, randInt))    trueResult = Request.queryPage(payload, place)
        if trueResult == kb.defaultResult:        payload = agent.payload(place, parameter, value, "%s%s AND %s%d=%d" % (value, ")" * parenthesis, "(" * parenthesis, randInt, randInt + 1))        falseResult = Request.queryPage(payload, place)        if falseResult != kb.defaultResult:            ......
    

    進行最終確認的代碼如下,由于這里是判斷數字型注入,注意上面的初步判斷使用的是randint隨機數字,而不是randstr隨機字符串。下方隨機的字符串構造的payload在存在數字注入的情況下不可能注入成功,根據這個條件最終確認數字注入

              payload = agent.payload(place, parameter, value, "%s%s AND %s%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr))            falseResult = Request.queryPage(payload, place)
                if falseResult != kb.defaultResult:                ......                return "numeric"
    

    單雙引號類型的注入基本邏輯類似,最終確認payload如下,and后的條件也是不可能滿足的

                payload = agent.payload(place, parameter, value, "%s'%s and %s%s" % (value, ")" * parenthesis, "(" * parenthesis, randStr))
    

    最終判斷出注入類型會添加到injData中,如果有多個注入點會調用__selectInjection讓用戶自行選擇一個


     if injType:        injData.append((place, parameter, injType)) ......if len(injData) == 1:    injDataSelected = injData[0]elif len(injData) > 1:    injDataSelected = __selectInjection(injData)
    checkForParenthesis()檢查最終是幾個括號進行閉合的。createTargetDirs()函數創建輸出目錄。action()是核心部分的函數if condition:    checkForParenthesis()    createTargetDirs()    action()
    

    檢測DBMS

    action()函數首先在確認目標DBMS,因為不同數據庫的語句和注入方式都有區別,首先初始化Handler,最后調用getFingerprint()方法

    conf.dbmsHandler = setHandler()......conf.dbmsHandler.getFingerprint()
    

    setHandler()中具體識別的插件是這里的每個Map。遍歷dbmsMap拿到Map插件,直接()調用,并在后續使用checkDbms()函數進行檢測

       dbmsMap   = (                  ( MYSQL_ALIASES, MySQLMap ),                  ( ORACLE_ALIASES, OracleMap ),                  ( PGSQL_ALIASES, PostgreSQLMap ),                  ( MSSQL_ALIASES, MSSQLServerMap ),                )
        for dbmsAliases, dbmsEntry in dbmsMap:        if conf.dbms and conf.dbms not in dbmsAliases:            debugMsg  = "skipping to test for %s" % dbmsNames[count]            logger.debug(debugMsg)            count += 1            continue
            dbmsHandler = dbmsEntry()
            if dbmsHandler.checkDbms():            if not conf.dbms or conf.dbms in dbmsAliases:                kb.dbmsDetected = True
                    return dbmsHandler
        return None
    

    注意到一個基類,各種數據庫的識別插件都繼承自此類,其中的escape和unescape主要做編碼和解碼的作用

    class Fingerprint:    @staticmethod    def unescape(expression)    @staticmethod    def escape(expression)    def getFingerprint(self)    def checkDbms(self)
    

    無需具體分析每一個DBMS,可以重點關注大家最常用的MySQL,它的初始化又調用了Enumeration,無需關心,只是簡單的一個類,包含很多MySQL相關的屬性

    class MySQLMap(Fingerprint, Enumeration, Filesystem, Takeover):    def __init__(self):        self.excludeDbsList = MYSQL_SYSTEM_DBS        Enumeration.__init__(self, "MySQL")
            unescaper.setUnescape(MySQLMap.unescape)
    

    跟入MySQL的checkDbms(),首先就看到大家比較熟悉的一個細節,判斷是否大于5.0,因為MySQL5.0以上有至關重要的information_schema

    if int(kb.dbmsVersion[0]) >= 5:    self.has_information_schema = True
    

    初步判斷版本邏輯,根據CONCAT語法邏輯進行判斷。其中inject.getValue這個函數很復雜,后續分析,現在認為它是根據注入的語句返回注入的結果即可。這里有一個小坑:randInt * 2是什么意思?如果randInt是1,那么答案應該是11而不是2,因為randInt = str(randomInt(1))

    randInt = str(randomInt(1))query = "CONCAT('%s', '%s')" % (randInt, randInt)
    if inject.getValue(query) == (randInt * 2):    logMsg = "confirming MySQL"
    

    使用LENGTH函數再次確認

    query = "LENGTH('%s')" % randInt
    if not inject.getValue(query) == "1":    warnMsg = "the back-end DMBS is not MySQL"
    

    嘗試從information_schema獲取數據,如果可以拿到,說明是MySQL5.0以上

    if inject.getValue("SELECT %s FROM information_schema.TABLES LIMIT 0, 1" % randInt) == randInt:    setDbms("MySQL 5")    self.has_information_schema = True
    

    MySQL6某些小版本的檢測。例如PARAMETERS表存放這存儲過程和存儲函數的參數信息以及存儲函數的返回值,及我們一般意義上的存儲過程和函數;PROFILING表提供了語句分析信息。這兩個表分別在6.0.5和6.0.3版本提供


    if inject.getValue("SELECT %s FROM information_schema.PARAMETERS LIMIT 0, 1" % randInt) == randInt:                    if inject.getValue("SELECT %s FROM information_schema.PROFILING LIMIT 0, 1" % randInt) == randInt:                        kb.dbmsVersion = [">= 6.0.5"]                    else:                        kb.dbmsVersion = [">= 6.0.3", "< 6.0.5"]
    

    后續的代碼可以跳過了,都是根據information_schema中某些表是否存在進行精確版本判斷

    最后一個else使用了我們常用的函數self.banner = inject.getValue("VERSION()")

    判斷結束后,會在conf.dbmsHandler.getFingerprint()中格式化輸出,而格式化輸出中有再次校驗DBMS的一個函數__commentCheck,這里用到一個技術正是大家繞WAF常用的:內斂版本注釋。首先/* NoValue */請求確認響應和默認響應一致,然后構造內斂版本注釋判斷語句是否能正常執行,對版本信息進行再次確認

    query   = agent.prefixQuery("/* NoValue */")query   = agent.postfixQuery(query)payload = agent.payload(newValue=query)result  = Request.queryPage(payload)
    if result != kb.defaultResult:    warnMsg = "unable to perform MySQL comment injection"    logger.warn(warnMsg)
        return None
    # MySQL valid versions updated at 10/2008versions = (    (32200, 32233),    # MySQL 3.22    (32300, 32354),    # MySQL 3.23    (40000, 40024),    # MySQL 4.0    (40100, 40122),    # MySQL 4.1    (50000, 50072),    # MySQL 5.0    (50100, 50129),    # MySQL 5.1    (60000, 60008),    # MySQL 6.0)......randInt = randomInt()version = str(version)query   = agent.prefixQuery("/*!%s AND %d=%d*/" % (version, randInt, randInt + 1))query   = agent.postfixQuery(query)payload = agent.payload(newValue=query)result  = Request.queryPage(payload)
    if result == kb.defaultResult:    ......
    確認完DBMS之后,將進行具體的注入,下一篇文章將分析,順便分析至關重要的inject.getValue是如何做到傳入一個注入表達式得到結果的
    
    
    sqlmap源碼分享
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    作為web滲透界的神器之一,無論是挖掘src或者滲透測試,不少的師傅們都離不開這個工具。他的強大也不只是簡單地自動化注入,后續文章我會逐漸帶大家熟悉這個工具的原理。其實網上已有大佬做了很多的分析,我將更細致更基礎地進行分析。
    burp0_data = {"name": username, "pw": password, "repw": password, "email": email, "submit": ''}
    如果大家條件允許的情況下可以先試著自己搭建自己先搞一遍,再來看這篇文章!
    主要是可以拿著這些信息通過goole,或github搜索一些其他的敏感信息,擴大搜索面。效果就不多說了,在github泄漏一些賬號或源碼的事件簡直不要太多。)如果得到的ip結果不同,即可判斷使用了CDN。nmap掃描服務器進行搜集,我認為也是至關重要的一點,不能遺漏。里面的security項rename-command CONFIG ""又問:如果內容禁止使用ip如何探測內網端口1、使用dns解析2、127。
    雖然市面上關于SSTI的題大都出在python上,但是這種攻擊方式請不要認為只存在于 Python 中,凡是使用模板的地方都可能會出現 SSTI 的問題,SSTI 不屬于任何一種語言。
    0x01 前言碰到了一個對外宣傳是否安全的站點,但實際測試下來并不安全。不過在這次獲取權限的過程中還是有點曲折,記錄下來并分享給大家。整個測試過程均在授權的情況下完成,漏洞詳細已經提交并通告相關知情。嘗試訪問后發現不能被解析只能下載。
    成功getshell后通過冰蝎上傳了一個哥斯拉shell接下來就是socks5代理了,上傳一個frp后發現服務端關了,事發突然并沒有做什么權限維持,到手的shell飛了經過分析和思考,造成這種情況的原因是直接拿了編譯好的frp沒做免殺,也許內網有全流量,設備報警提醒了,管理員發現異常后直接關機了。
    滲透測試很多時候需要的細心和耐心再加上一點運氣
    雖說目前互聯網上已經有很多關于 sql 注入的神器了,但是在這個 WAF 橫行的時代,手工注入往往在一些真實環境中會顯得尤為重要。這只是一個簡單的總結,只是簡單的為新手分享一下SQL注入,文中內容可能會存在錯誤,望大佬們手下留情!0x01 Mysql 手工注入1.1 聯合注入?id=0' union select 1,2,3,group_concat from users --+#group_concat 可替換為 concat_ws
    如下圖:掃碼后發現跳轉到了QQ郵箱登陸界面,確定為釣魚網站,看到其域名為http://****kak2.cn。既然是將數據提交到本站了,那么如果釣魚者再后端接收數據時直接將參數拼接到SQL語句中,那么就可能存在SQL注入。現在我們構造數據,提交數據,然后抓取數據包來進行測試,抓取的數據包如下:接下來開始測試是否存在SQL注入,name參數后添加單引號,發送數據,發現報錯,存在SQL注入!猜解一下數據庫名,數據庫版本,構造payload' and updatexml%23
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类