介绍
作为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/2008
versions = (
(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
是如何做到传入一个注入表达式得到结果的