Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ jobs:
# 'binary' instead of 'text'. Keeping this step byte-compile-free leaves --smoke clean.
run: python -B -m unittest discover -s tests -p "test_*.py"

- name: Esperanto self-test
# offline, deterministic engine check against an in-memory SQLite boolean oracle: all
# compare modes + identify + bytes/text + noisy-oracle quorum + integrity + strategy
# handoff (a failed assertion exits non-zero)
run: python extra/esperanto/run.py --self-test

- name: Coverage
if: matrix.python-version != 'pypy-2.7'
run: |
Expand Down
4 changes: 2 additions & 2 deletions data/xml/queries.xml
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@
<blind query="SELECT OWNER FROM (SELECT OWNER,ROWNUM AS CAP FROM (SELECT DISTINCT(OWNER) FROM SYS.ALL_TABLES)) WHERE CAP=%d" count="SELECT COUNT(DISTINCT(OWNER)) FROM SYS.ALL_TABLES"/>
</dbs>
<tables>
<inband query="SELECT OWNER,OBJECT_NAME FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE IN ('TABLE','VIEW')" condition="OWNER"/>
<blind query="SELECT OBJECT_NAME FROM (SELECT OBJECT_NAME,ROWNUM AS CAP FROM SYS.ALL_OBJECTS WHERE OWNER='%s' AND OBJECT_TYPE IN ('TABLE','VIEW')) WHERE CAP=%d" count="SELECT COUNT(OBJECT_NAME) FROM SYS.ALL_OBJECTS WHERE OWNER='%s' AND OBJECT_TYPE IN ('TABLE','VIEW')"/>
<inband query="SELECT OWNER,OBJECT_NAME FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE IN ('TABLE','VIEW') AND OBJECT_NAME NOT LIKE 'BIN$%%'" condition="OWNER"/>
<blind query="SELECT OBJECT_NAME FROM (SELECT OBJECT_NAME,ROWNUM AS CAP FROM SYS.ALL_OBJECTS WHERE OWNER='%s' AND OBJECT_TYPE IN ('TABLE','VIEW') AND OBJECT_NAME NOT LIKE 'BIN$%%') WHERE CAP=%d" count="SELECT COUNT(OBJECT_NAME) FROM SYS.ALL_OBJECTS WHERE OWNER='%s' AND OBJECT_TYPE IN ('TABLE','VIEW') AND OBJECT_NAME NOT LIKE 'BIN$%%'"/>
</tables>
<columns>
<inband query="SELECT COLUMN_NAME,DATA_TYPE FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='%s' AND OWNER='%s'" condition="COLUMN_NAME"/>
Expand Down
28 changes: 28 additions & 0 deletions extra/esperanto/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission

esperanto - a DBMS-agnostic SQL poking prototype.

Given only a boolean oracle - a callable oracle(condition) -> bool that reports
whether an arbitrary SQL boolean expression holds at the target - this discovers
the target's SQL dialect from scratch (concatenation operator, substring / length
/ char-code functions, string comparison, catalog surface) and then extracts data
char-by-char WITHOUT ever being told which DBMS is on the other end.

The candidate variants below are harvested from sqlmap's own data/xml/queries.xml
across all 31 supported DBMSes - the point is to reuse that accumulated knowledge
as a single "esperanto" the tool speaks at any backend, rather than fingerprinting
first and loading one dialect.

This is a self-contained research prototype (no sqlmap imports); run it directly
for a built-in self-test against an in-memory SQLite oracle.
"""

from .engine import Esperanto
from .engine import hostExtract
from .handler import buildHandler
from .records import Cap, ExtractResult, BulkResult, Dialect, InferenceStrategy
from .records import OracleUndecided, QueryBudgetExceeded

__all__ = ["Esperanto", "hostExtract", "buildHandler", "Cap", "ExtractResult", "BulkResult", "Dialect", "InferenceStrategy", "OracleUndecided", "QueryBudgetExceeded"]
609 changes: 609 additions & 0 deletions extra/esperanto/__main__.py

Large diffs are not rendered by default.

437 changes: 437 additions & 0 deletions extra/esperanto/atlas.py

Large diffs are not rendered by default.

484 changes: 484 additions & 0 deletions extra/esperanto/discovery.py

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions extra/esperanto/engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python

"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""

from .atlas import _FREQ_ORDER
from .atlas import _PRINTABLE_SORTED
from .atlas import _REPL
from .atlas import _unichr
from .records import Dialect
from .oracle import _OracleCore
from .discovery import _Discovery
from .extraction import _Extraction
from .enumeration import _Enumeration


class Esperanto(_OracleCore, _Discovery, _Extraction, _Enumeration):
"""DBMS-agnostic blind extractor. Behaviour lives in four mixins by concern
(oracle / discovery / extraction / enumeration); this class only holds
construction + the query counter."""

def __init__(self, oracle, verbose=False, maxlen=4096, retries=1, quorum=1,
maxbytes=None, max_queries=None):
"""oracle: callable(condition_str) -> bool.
quorum>1 turns on majority voting (2*quorum-1 samples) so a noisy or
intermittently-erroring oracle can't flip a single probe and corrupt a
result; retries re-attempts a raised call before it counts as an error.
maxlen caps text characters; maxbytes separately caps byte/hex recovery
(default 4*maxlen); max_queries is an optional hard oracle-call ceiling."""
if not callable(oracle):
raise TypeError("oracle must be callable")
self.oracle = oracle
self.verbose = verbose
self.maxlen = maxlen
self.maxbytes = maxlen * 4 if maxbytes is None else maxbytes
self.retries = retries
self.quorum = max(1, quorum)
self.max_queries = max_queries
self.dialect = Dialect()
self._queries = 0
self._errors = 0
self._hexProbed = False
self._hexOrdered = None
self._backslashEscape = None
self._codeTmpl = None
self._comparator = "gt" # ordered-compare op: "gt" / "between" / "membership"
self._inOk = True # IN(...) usable (order-free subset bisection)
self._lastTruncated = False
self._discovered = False
self._probing = False # True while laddering CANDIDATE rungs (discovery or lazy _ensure*): an
# undecidable probe there = "rung unusable" -> False; elsewhere (reading
# committed data) an undecidable probe stays undecided so it degrades loudly
self._progress = None # optional host callback(str) for live feedback

@property
def queryCount(self):
return self._queries


def hostExtract(oracle, strategy, expr, maxlen=4096):
"""Reference HOST inference loop driven ONLY by an InferenceStrategy + oracle.

This is the proof that the strategy is a sufficient hand-off: it reproduces
char-by-char extraction with zero dependency on Esperanto's own retrieval code -
exactly what sqlmap's `bisection()`/`queryOutputLength()` would do instead, but in
~30 lines. Covers the char-comparison modes (code / collation / ordinal /
equality); hex mode is reachable the same way via strategy.renderHex()."""
ask = lambda cond: bool(oracle(cond))
L = strategy.renderLength(expr)
if not ask("%s>=0" % L):
return None
if ask("%s=0" % L):
return ""
lo, hi = 1, min(8, maxlen)
while hi < maxlen and ask("%s>%d" % (L, hi)):
lo, hi = hi + 1, min(hi * 2, maxlen)
while lo < hi:
mid = (lo + hi) // 2
lo, hi = (mid + 1, hi) if ask("%s>%d" % (L, mid)) else (lo, mid)
length = lo

def read(pos):
mode = strategy.compare_mode
if mode == "code":
code = strategy.renderCode(expr, pos)
top = 0x10FFFF
for cap in (127, 255, 0xFFFF, 0x10FFFF):
if not ask("%s>%d" % (code, cap)):
top = cap
break
a, b = 0, top
while a < b:
m = (a + b) // 2
a, b = (m + 1, b) if ask("%s>%d" % (code, m)) else (a, m)
return _unichr(a)
if mode in ("collation", "ordinal"):
cs = _PRINTABLE_SORTED
a, b = 0, len(cs) - 1
while a < b:
m = (a + b) // 2
a, b = (m + 1, b) if ask(strategy.renderCharCmp(expr, pos, cs[m], ">")) else (a, m)
return cs[a]
for ch in _FREQ_ORDER: # equality scan
if ask(strategy.renderCharCmp(expr, pos, ch, "=")):
return ch
return _REPL

return "".join(read(i) for i in range(1, length + 1))
Loading