【技术分享】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
网络安全专家