Merge branch 'master' into hana-pr

# Conflicts:
#	data/txt/sha256sums.txt
#	lib/core/common.py
#	lib/core/settings.py
This commit is contained in:
Miroslav Štampar 2026-07-09 16:48:30 +02:00
commit 1465d55a47
239 changed files with 57035 additions and 4345 deletions

View file

@ -51,6 +51,7 @@ from lib.core.settings import DEFAULT_GET_POST_DELIMITER
from lib.core.settings import GENERIC_SQL_COMMENT
from lib.core.settings import GENERIC_SQL_COMMENT_MARKER
from lib.core.settings import INFERENCE_MARKER
from lib.core.settings import MYSQL_UNION_VALUE_CAST
from lib.core.settings import NULL
from lib.core.settings import PAYLOAD_DELIMITER
from lib.core.settings import REPLACEMENT_MARKER
@ -69,9 +70,9 @@ class Agent(object):
query = self.cleanupPayload(query)
if query.upper().startswith("AND "):
query = re.sub(r"(?i)AND ", "SELECT ", query, 1)
query = re.sub(r"(?i)AND ", "SELECT ", query, count=1)
elif query.upper().startswith(" UNION ALL "):
query = re.sub(r"(?i) UNION ALL ", "", query, 1)
query = re.sub(r"(?i) UNION ALL ", "", query, count=1)
elif query.startswith("; "):
query = query.replace("; ", "", 1)
@ -220,7 +221,7 @@ class Agent(object):
elif BOUNDED_INJECTION_MARKER in paramDict[parameter]:
if base64Encoding:
retVal = paramString.replace("%s%s" % (_origValue, BOUNDED_INJECTION_MARKER), _newValue)
match = re.search(r"(%s)=([^&]*)" % re.sub(r" \(.+", "", parameter), retVal)
match = re.search(r"(%s)=([^&]*)" % re.escape(re.sub(r" \(.+", "", parameter)), retVal)
if match:
retVal = retVal.replace(match.group(0), "%s=%s" % (match.group(1), encodeBase64(match.group(2), binary=False, encoding=conf.encoding or UNICODE_ENCODING)))
else:
@ -676,6 +677,49 @@ class Agent(object):
pass
return retVal
@staticmethod
def _collapseFieldDelimiterSpace(query):
"""
Collapses ", " into "," to normalize the column-list delimiter, but ONLY outside
single/double quoted string literals, so a comma-space inside a literal (e.g. in a
WHERE clause: name='John, Jr') is preserved verbatim. The quote/escape handling
mirrors splitFields()/zeroDepthSearch().
>>> Agent._collapseFieldDelimiterSpace("SELECT a, b FROM t")
'SELECT a,b FROM t'
>>> Agent._collapseFieldDelimiterSpace("SELECT a, b FROM t WHERE name='John, Jr'")
"SELECT a,b FROM t WHERE name='John, Jr'"
"""
retVal = []
quote = None
index = 0
length = len(query)
while index < length:
char = query[index]
if quote:
retVal.append(char)
if char == quote:
if index + 1 < length and query[index + 1] == quote: # escaped quote (e.g. '')
retVal.append(query[index + 1])
index += 2
continue
else:
quote = None
elif char in ('"', "'"):
quote = char
retVal.append(char)
elif char == ',' and index + 1 < length and query[index + 1] == ' ':
retVal.append(',') # keep the delimiter, drop the single trailing space
index += 2
continue
else:
retVal.append(char)
index += 1
return "".join(retVal)
def concatQuery(self, query, unpack=True):
"""
Take in input a query string and return its processed nulled,
@ -704,7 +748,7 @@ class Agent(object):
if unpack:
concatenatedQuery = ""
query = query.replace(", ", ',')
query = self._collapseFieldDelimiterSpace(query)
fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsSelectTop, fieldsSelectCase, _, fieldsToCastStr, fieldsExists = self.getFields(query)
castedFields = self.nullCastConcatFields(fieldsToCastStr)
concatenatedQuery = query.replace(fieldsToCastStr, castedFields, 1)
@ -825,9 +869,9 @@ class Agent(object):
return concatenatedQuery
def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None):
def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None, collate=False):
"""
Take in input an query (pseudo query) string and return its
Take in input a query (pseudo query) string and return its
processed UNION ALL SELECT query.
Examples:
@ -867,10 +911,21 @@ class Agent(object):
if query.startswith("SELECT "):
query = query[len("SELECT "):]
# On MySQL 8+ the retrieved value (connection collation) cannot be merged in a
# UNION column with a table column of a different collation (e.g. utf8mb4_0900_ai_ci),
# raising "Illegal mix of collations". Normalizing the charset and forcing an explicit
# collation (highest coercibility) wins the merge (Note: skipped for NULL/numeric values).
# Note: requires the utf8mb4 charset (MySQL >= 5.5.3) used in MYSQL_UNION_VALUE_CAST; on
# older versions there is no such collation clash to begin with (unknown version => assumed recent).
collateField = collate and Backend.isDbms(DBMS.MYSQL) and isDBMSVersionAtLeast('5.5.3') is not False
def _collate(value):
return MYSQL_UNION_VALUE_CAST % value if collateField and value and value != NULL and not value.isdigit() else value
unionQuery = self.prefixQuery("UNION ALL SELECT ", prefix=prefix)
if limited:
unionQuery += ','.join(char if _ != position else '(SELECT %s)' % query for _ in xrange(0, count))
unionQuery += ','.join(char if _ != position else _collate('(SELECT %s)' % query) for _ in xrange(0, count))
unionQuery += fromTable
unionQuery = self.suffixQuery(unionQuery, comment, suffix)
@ -900,12 +955,22 @@ class Agent(object):
else:
infoFile = None
if not infoFile:
query = _collate(query)
# A fuzzy-discovered per-column type template (kb.unionTemplate, e.g. ['1234', '%s', '5678'])
# forces type-compatible fillers on strict DBMSes (e.g. Apache Derby, which rejects bare NULL
# and demands UNION column-type parity); '%s' marks the slot carrying the injected expression.
template = kb.unionTemplate if isinstance(kb.unionTemplate, (list, tuple)) and len(kb.unionTemplate) == count else None
for element in xrange(0, count):
if element > 0:
unionQuery += ','
if conf.uValues and conf.uValues.count(',') + 1 == count:
unionQuery += conf.uValues.split(',')[element]
elif template is not None:
unionQuery += query if template[element] == "%s" else template[element]
elif element == position:
unionQuery += query
else:
@ -927,8 +992,10 @@ class Agent(object):
if element > 0:
unionQuery += ','
if element == position:
unionQuery += multipleUnions
if template is not None:
unionQuery += _collate(multipleUnions) if template[element] == "%s" else template[element]
elif element == position:
unionQuery += _collate(multipleUnions)
else:
unionQuery += char
@ -964,7 +1031,9 @@ class Agent(object):
stopLimit = limitRegExp.group(int(limitGroupStop))
elif limitRegExp2:
startLimit = 0
stopLimit = limitRegExp2.group(int(limitGroupStart))
# Note: query2 (LIMIT without OFFSET) always has exactly one group (the
# count); using limitGroupStart here would IndexError for H2 (groupstart=2)
stopLimit = limitRegExp2.group(1)
limitCond = int(stopLimit) > 1
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
@ -1066,7 +1135,7 @@ class Agent(object):
original = query.split("SELECT ", 1)[1].split(" FROM", 1)[0]
for part in original.split(','):
if re.search(r"\b%s\b" % re.escape(field), part):
_ = re.sub(r"SELECT.+?FROM", "SELECT %s AS z,row_number() over() AS y FROM" % part, query, 1)
_ = re.sub(r"SELECT.+?FROM", "SELECT %s AS z,row_number() over() AS y FROM" % part, query, count=1)
replacement = "SELECT x.z FROM (%s)x WHERE x.y-1=%d" % (_, num)
limitedQuery = replacement
break
@ -1117,7 +1186,7 @@ class Agent(object):
limitedQuery = safeStringFormat(limitedQuery, (fromFrom,))
limitedQuery += "=%d" % (num + 1)
elif Backend.isDbms(DBMS.MSSQL):
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
forgeNotIn = True
if " ORDER BY " in limitedQuery:
@ -1266,7 +1335,10 @@ class Agent(object):
if Backend.isDbms(DBMS.ORACLE) and re.search(r"qq ORDER BY \w+\)", query, re.I) is not None:
prefix, suffix = re.sub(r"(?i)(qq)( ORDER BY \w+\))", r"\g<1> WHERE %s\g<2>" % conf.dumpWhere, query), ""
else:
match = re.search(r" (LIMIT|ORDER).+", query, re.I)
# Note: require a genuine trailing clause (ORDER BY / LIMIT word-bounded), so a
# column/identifier merely starting with "order"/"limit" (e.g. order_id) is not
# mistaken for the suffix and the WHERE is not spliced into the wrong place
match = re.search(r" (ORDER\s+BY\b|LIMIT\b).+", query, re.I)
if match:
suffix = match.group(0)
prefix = query[:-len(suffix)]