sqlmap/extra/esperanto/records.py
2026-07-22 01:06:57 +02:00

319 lines
15 KiB
Python

#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
class OracleUndecided(RuntimeError):
"""The oracle gave no reliable True/False after retries/voting - a transport or
observation failure, NOT a definitive answer. Raised so blind extraction fails
CLOSED instead of converging on plausible-but-wrong data from a manufactured
False. (A wrong-dialect/unsupported probe must be reported as False by the
oracle itself; exceptions are reserved for 'could not observe'.)"""
class NumericOutOfRange(OverflowError):
"""A bounded numeric read proved the value lies OUTSIDE [lo, hi] (above or below).
Raised so a bounded search never invents an in-range boundary value (saturating to
`hi`, or - for BETWEEN - converging to a wrong small value). Callers decide: length
measurement converts it to (ceiling, truncated=True); integer extraction re-raises."""
class Cap(object):
"""A discovered primitive: (name, template) PLUS measured semantic properties.
Indexable like the old (name, template) tuple so existing call sites keep working
(`cap[0]`, `cap[1]`), with `cap.props` / `cap.get(key)` for the measured facts -
e.g. length unit=characters|bytes, substring index_base, charcode semantics."""
__slots__ = ("name", "template", "props")
def __init__(self, name, template, **props):
self.name, self.template, self.props = name, template, props
def __getitem__(self, i):
return (self.name, self.template)[i]
def get(self, key, default=None):
return self.props.get(key, default)
def __repr__(self):
extra = (" " + " ".join("%s=%s" % kv for kv in sorted(self.props.items()))) if self.props else ""
return "%s(%s)" % (self.name, extra.strip() or self.template)
class Integrity(object):
"""How much the engine PROVED about a recovered value - separating 'the walk finished'
from 'the bytes are exactly the source'. `complete` alone conflated the two; a value can
be WHOLE (every position visited) yet not EXACT (case/accent ambiguous under a lossy
collation). Anything used as executable SQL metadata or a paging boundary requires EXACT."""
EXACT = "exact" # proven byte-identical to the source value (or a proven NULL)
WHOLE_BUT_AMBIGUOUS = "ambiguous" # every position visited, but case/accent is uncertain
TRUNCATED = "truncated" # a bounded prefix; the source continues
UNRESOLVED = "unresolved" # a character could not be recovered (U+FFFD present)
FAILED = "failed" # could not observe / verify (e.g. whole-value check failed)
# warnings that leave a value WHOLE but not EXACT (case/accent uncertain) - distinct from the
# "recovered via hex" soft note (that value IS exact) and from hard integrity warnings.
_AMBIGUOUS_WARNINGS = ("case", "collation")
_U_REPL = u"\uFFFD"
class ExtractResult(object):
"""Structured extraction outcome - keeps NULL, empty, truncated and failed
distinct (str-like so `str(r)`/truthiness still read naturally)."""
__slots__ = ("value", "is_null", "complete", "truncated", "queries", "warnings")
def __init__(self, value, is_null=False, complete=True, truncated=False, queries=0, warnings=None):
self.value = value
self.is_null = is_null
self.complete = complete
self.truncated = truncated
self.queries = queries
self.warnings = warnings or []
@property
def integrity(self):
# classify what was PROVED (see Integrity). Order matters: a HARD defect (truncated /
# unresolved / incomplete) is classified BEFORE null-exactness, so an inconsistent
# (value=None, is_null=True, complete=False) can never read as EXACT.
if self.truncated:
return Integrity.TRUNCATED
if self.value is not None and _U_REPL in self.value:
return Integrity.UNRESOLVED
if not self.complete: # a hard warning / failed verify -> not exact
return Integrity.FAILED
if self.value is None:
return Integrity.EXACT if self.is_null else Integrity.FAILED
if any(a in w for w in self.warnings for a in _AMBIGUOUS_WARNINGS):
return Integrity.WHOLE_BUT_AMBIGUOUS
return Integrity.EXACT
@property
def exact(self):
# True ONLY when the recovered bytes are proven identical to the source. Required for
# any value that becomes executable SQL (identifier, qualifier, exact literal).
return self.integrity == Integrity.EXACT
def __str__(self):
return "" if self.value is None else self.value
def __bool__(self):
return bool(self.value)
__nonzero__ = __bool__ # py2
def __repr__(self):
return ("ExtractResult(value=%r null=%s integrity=%s truncated=%s q=%d%s)"
% (self.value, self.is_null, self.integrity, self.truncated, self.queries,
" warnings=%r" % self.warnings if self.warnings else ""))
class BulkResult(object):
"""List-like bulk-enumeration outcome that also reports completeness against an
independent COUNT (so a truncated dump is visible, not silently short)."""
__slots__ = ("values", "expected", "complete")
def __init__(self, values, expected=None, complete=True):
self.values = values
self.expected = expected
self.complete = complete
def __iter__(self):
return iter(self.values)
def __len__(self):
return len(self.values)
def __getitem__(self, i):
return self.values[i]
def __repr__(self):
return "%r%s" % (self.values, "" if self.complete else " (incomplete: %s of %s)" % (len(self.values), self.expected))
class Dialect(object):
"""Discovered target profile - the synthesized 'queries.xml row'."""
__slots__ = ("concat", "substring", "length", "bytelen", "textcast",
"coalesce", "charcode", "charfrom", "hexfn", "binwrap", "charset",
"bulkAgg", "dual", "identQuote", "prefix", "compare", "ordered",
"identity", "catalog", "catalogEnum", "family", "product",
"version", "evidence", "notes")
def __init__(self):
self.identQuote = None # (open, close) identifier-quote chars, or None
self.prefix = None # (op, multi, single) LIKE/GLOB fallback, or None
self.concat = None # (name, template)
self.substring = None
self.length = None
self.bytelen = None # (name, template) byte length (vs char length)
self.textcast = None # (name, template) scalar -> text
self.coalesce = None # (name, template) NULL guard
self.charcode = None # None -> no code fn (direct-compare mode)
self.charfrom = None
self.hexfn = None # (name, template) for hex/byte extraction
self.binwrap = None # (name, template) byte-ordered comparison wrapper
self.charset = None # Python codec for the DB's DECLARED charset (authoritative hex decode)
self.bulkAgg = None # (name, template) row-aggregation for bulk enum
self.dual = None # (name, from-suffix) tableless-SELECT skeleton
self.compare = None # 'code' | 'collation' | 'hex' | 'ordinal' | 'equality' | 'equality-ci'
self.ordered = False # 'b' > 'a' holds (lexicographic bisection ok)
self.identity = {}
self.catalog = None
self.catalogEnum = {} # {kind: (name_col, source, filter)}
self.family = None # from the catalog probe
self.product = None # best-guess product (the detective verdict)
self.version = None # extracted banner string
self.evidence = [] # [(signal, implication), ...]
self.notes = []
def __repr__(self):
pick = lambda x: x[0] if x else None
return ("Dialect(concat=%r substring=%r length=%r compare=%r dual=%r "
"catalog=%r product=%r)" % (
pick(self.concat), pick(self.substring), pick(self.length),
self.compare, pick(self.dual), self.catalog,
self.product or self.family))
class InferenceStrategy(object):
"""An immutable, host-consumable rendering of a discovered dialect.
This is the crystallization esperanto is really for: the discovery half produces
ONE frozen strategy, and a host inference engine (ideally sqlmap's existing
bisection - which already owns threading, hashDB resume, prediction, and the
known-answer reliability litmus) drives the loop by calling these PURE render_*
methods. No oracle here, no retrieval loop - just SQL construction from the
discovered primitives. Frozen after construction so worker threads can share it.
The reference host loop `hostExtract()` below drives extraction using ONLY a
strategy + an oracle, with no dependency on Esperanto's own retrieval code.
SCOPE (honest): this is a PARTIAL hand-off, not yet a full sufficiency proof.
`hostExtract()` drives the code/collation/ordinal/equality char modes under an
ordered comparator (gt / BETWEEN / operator-free), with length-fn OR substring-
derived length, tri-state and integrity-carrying. It does NOT yet drive pattern-only
(LIKE floor) or membership-only extraction, and the frozen field set does not yet
carry every discovered semantic (hex-encoding confidence, IN support, wildcard
semantics, substring/length units, cast bounds). Those modes/semantics must be added
- or the claim narrowed - before this can be called a complete interface; the
long-term direction is predicate renderers driven by sqlmap's own inference engine.
"""
_FIELDS = ("product", "family", "compare_mode", "catalog", "dual", "notes",
"substring", "index_base", "length", "charcode", "charcode_sem",
"hexfn", "binwrap", "charfrom", "concat", "identquote", "backslash",
"comparator", "cmp_template", "exact_witness")
def __init__(self, **kw):
for f in self._FIELDS:
object.__setattr__(self, f, kw.get(f))
object.__setattr__(self, "_frozen", True)
def __setattr__(self, *a): # immutable
raise AttributeError("InferenceStrategy is frozen")
# -- pure SQL construction (no oracle) ----------------------------------
def substr(self, expr, pos, length=1):
p = pos if self.index_base == 1 else pos - 1
return self.substring.format(expr=expr, pos=p, len=length)
def renderLength(self, expr):
return self.length.format(expr=expr)
def renderIsNull(self, expr):
return "(%s) IS NULL" % expr
def renderCharExists(self, expr, pos):
# a character exists at 1-based pos: its 1-char substring is non-empty. Uses '='
# only (no '>'/'<'), and reads False for a past-end substring whether the engine
# returns '' or NULL there - so length can be derived when there is no length fn.
return "NOT ((%s)='')" % self.substr(expr, pos)
def renderHex(self, expr):
return self.hexfn.format(expr=expr) if self.hexfn else None
def renderCode(self, expr, pos):
# scalar code point of the char at pos (None unless a code fn was found)
return self.charcode.format(expr=self.substr(expr, pos)) if self.charcode else None
def renderGt(self, expr, n, high=None):
# "expr > n" via the DISCOVERED comparator, so a host driving this strategy
# survives a WAF that strips '>'/'<' exactly as esperanto's own loop does.
# BETWEEN needs the current upper bound; operator-free rungs (sign/abs/...) don't.
if self.comparator == "between" and high is not None:
return "%s BETWEEN %d AND %d" % (expr, n + 1, high)
if self.cmp_template:
return self.cmp_template.format(expr=expr, n=n)
return "%s>%d" % (expr, n)
def renderCharCmp(self, expr, pos, ch, op=">"):
# boolean: char at pos <op> literal ch, byte-ordered when a binary wrapper
# is available (else the target's own collation)
one = self.substr(expr, pos)
if self.binwrap:
return "%s%s%s" % (self.binwrap.format(x=one), op, self.binwrap.format(x=self.lit(ch)))
return "%s%s%s" % (one, op, self.lit(ch))
def renderExactEq(self, left, right):
# whole-value byte-exact equality (hex > binary wrapper > plain)
if self.hexfn:
return "(%s)=(%s)" % (self.hexfn.format(expr=left), self.hexfn.format(expr=right))
if self.binwrap:
return "(%s)=(%s)" % (self.binwrap.format(x=left), self.binwrap.format(x=right))
return "(%s)=(%s)" % (left, right)
def lit(self, value):
s = value.replace("\\", "\\\\") if self.backslash else value
return "'%s'" % s.replace("'", "''")
def buildLiteral(self, value):
if value and self.charfrom and self.concat and all(0 < ord(c) < 128 for c in value):
parts = [self.charfrom.format(code=ord(c)) for c in value]
out = parts[0]
for p in parts[1:]:
out = self.concat.format(a=out, b=p)
return out
return self.lit(value)
def quoteIdent(self, name):
if not self.identquote:
return name
o, c = self.identquote
return "%s%s%s" % (o, name.replace(c, c * 2), c)
def asQueriesRow(self):
"""The sqlmap queries.xml-shaped mapping - the concrete integration hook.
These four templates are what sqlmap's inference/error/union machinery reads
from queries[Backend.getIdentifiedDbms()]. The `inference` template renders the
char-code comparison through the DISCOVERED comparator, so a strategy that survives
a '>'-stripping WAF natively is NOT turned back into a blocked '>' here. Membership
mode has no ordered `>%d` form, so `inference` is None (that mode needs the predicate
interface, not this 4-field row)."""
inference = None
if self.charcode:
code = self.charcode.format(expr=self.substr("%s", "%d"))
if self.comparator == "membership":
inference = None
elif self.cmp_template: # operator-free rung (SIGN/ABS/...)
inference = self.cmp_template.replace("{expr}", code).replace("{n}", "%d")
elif self.comparator == "between":
inference = "%s BETWEEN (%%d)+1 AND 9223372036854775807" % code
else: # gt
inference = "%s>%%d" % code
return {
"length": self.length,
"substring": self.substring,
"inference": inference,
"case": "SELECT (CASE WHEN (%s) THEN 1 ELSE 0 END)" + self.dual,
"hex": self.hexfn,
}
def __repr__(self):
return "InferenceStrategy(product=%r compare=%r catalog=%r)" % (
self.product, self.compare_mode, self.catalog)
class QueryBudgetExceeded(OracleUndecided):
"""The configured hard oracle-call budget was exhausted mid-operation."""