diff --git a/extra/esperanto/README.md b/extra/esperanto/README.md index a66e387f54b..a22ee07ec52 100644 --- a/extra/esperanto/README.md +++ b/extra/esperanto/README.md @@ -41,7 +41,7 @@ python run.py --self-test ``` The `*` (or an explicit `[INFERENCE]`) marks the injection point; without one it defaults to the end -of the URL. The true/false oracle is taken from `--string` / `--not-string` / `--code`, or +of the URL. The true/false oracle is taken from `--string` / `--code`, or auto-calibrated from the response when none is given. ## How it works diff --git a/extra/esperanto/__init__.py b/extra/esperanto/__init__.py index 45e5fa09e06..fd7121ddb54 100644 --- a/extra/esperanto/__init__.py +++ b/extra/esperanto/__init__.py @@ -19,10 +19,12 @@ for a built-in self-test against an in-memory SQLite oracle. """ +__version__ = "1.0.0" + from .engine import Esperanto from .engine import hostExtract from .handler import buildHandler from .records import Cap, ExtractResult, BulkResult, Dialect, InferenceStrategy, Integrity from .records import OracleUndecided, QueryBudgetExceeded -__all__ = ["Esperanto", "hostExtract", "buildHandler", "Cap", "ExtractResult", "BulkResult", "Dialect", "InferenceStrategy", "Integrity", "OracleUndecided", "QueryBudgetExceeded"] +__all__ = ["Esperanto", "hostExtract", "buildHandler", "Cap", "ExtractResult", "BulkResult", "Dialect", "InferenceStrategy", "Integrity", "OracleUndecided", "QueryBudgetExceeded", "__version__"] diff --git a/extra/esperanto/__main__.py b/extra/esperanto/__main__.py index ea6f116d535..e6c8ab46fa8 100644 --- a/extra/esperanto/__main__.py +++ b/extra/esperanto/__main__.py @@ -5,11 +5,14 @@ See the file 'LICENSE' for copying permission """ +from . import __version__ from .atlas import _REPL from .engine import Esperanto, hostExtract from .records import ( OracleUndecided, ExtractResult, QueryBudgetExceeded) +_SITE = "https://sqlmap.org" + # ratio-mode confidence margin: if the true/false similarity scores are within this of each # other the page can't be classified, so the oracle returns UNDECIDED rather than guessing. _RATIO_MARGIN = 0.05 @@ -331,14 +334,14 @@ def mssql(): return ok -def _httpOracle(url, data=None, cookie=None, headers=None, string=None, notString=None, code=None): +def _httpOracle(url, data=None, cookie=None, headers=None, string=None, code=None): """A boolean oracle over a real HTTP target, for standalone use. The condition is substituted at the injection marker: '[INFERENCE]' is replaced verbatim (you supply the context, e.g. `id=1 AND [INFERENCE]`), else a single '*' is replaced with ` AND ()`. True/false is decided by a reduced port of sqlmap's response differentiation - the - Pareto 80%: explicit --string/--not-string/--code win; otherwise it CALIBRATES from + Pareto 80%: explicit --string/--code win; otherwise it CALIBRATES from two true (1=1) baselines + one false (1=2) and auto-picks the cheapest reliable signal - HTTP status code, else a stable text line present in true but not false, else a difflib similarity ratio. Two noise-killers borrowed from sqlmap make it @@ -357,7 +360,7 @@ def _httpOracle(url, data=None, cookie=None, headers=None, string=None, notStrin if "[INFERENCE]" not in (url + (data or "")) and "*" not in (url + (data or "")): url = url + "*" # no marker given -> inject at the end of the URL by default - print("[*] no injection marker ('*' or '[INFERENCE]') given; defaulting to end of URL: %s" % url) + print("[i] no injection marker ('*' or '[INFERENCE]') given; defaulting to end of URL: %s" % url) # ONE kept-alive connection reused across every probe. a blind dump is thousands of # requests; opening a fresh TCP+TLS handshake per probe (what urlopen does) is ~0.2s of @@ -428,8 +431,6 @@ def _clean(body, cond): mode, wanted, base = None, None, {} if string is not None: mode = "string" - elif notString is not None: - mode = "notstring" elif code is not None: mode, wanted = "code", int(code) else: # auto-calibrate @@ -456,8 +457,8 @@ def _clean(body, cond): break if wanted is None: # (3) similarity ratio floor mode, base = "ratio", {"t": tc, "f": fc} - if not string and not notString: - print("[*] calibrated oracle: %s%s" % (mode, (" (%r)" % wanted) if mode in ("code", "autostring") else "")) + if string is None: + print("[i] calibrated oracle: %s%s" % (mode, (" (%r)" % wanted) if mode in ("code", "autostring") else "")) def classify(cond): body, status = fetch(cond) @@ -465,8 +466,6 @@ def classify(cond): return None # let the engine retry/degrade, never a fake bool if mode == "string": return string in body - if mode == "notstring": - return notString not in body if mode == "code": return status == wanted if mode == "autostring": @@ -599,7 +598,7 @@ def _report(esp, args): else: scope = cur if scope: - print("[*] scoping to database/schema: %s" % scope) + print("[i] scoping to database/schema: %s" % scope) if args.current_user: expr = esp.dialect.identity.get("user") print("[*] current user: %s" % (_scalar(esp, expr) if expr else "n/a")) @@ -607,35 +606,29 @@ def _report(esp, args): expr = esp.dialect.identity.get("database") print("[*] current database: %s" % (_scalar(esp, expr) if expr else "n/a")) if args.tables: - print("[*] fetching tables ...") + print("[i] fetching tables ...") print("[*] tables: %s" % ", ".join(esp.enumerate("table", schema=scope) or [""])) if args.columns: if not args.tbl: print("[!] --columns needs -T ") else: - print("[*] fetching columns for '%s' ..." % args.tbl) + print("[i] fetching columns for '%s' ..." % args.tbl) print("[*] columns of %s: %s" % (args.tbl, ", ".join(esp.columns(args.tbl, schema=scope) or [""]))) - if args.query: - print("[*] fetching %s ..." % args.query) - print("[*] %s = %s" % (args.query, _scalar(esp, args.query))) if args.dump: if not args.tbl: print("[!] --dump needs -T
") else: cols = [c.strip() for c in args.col.split(",")] if args.col else None if cols is None: # enumerate columns FIRST (own phase), so the - print("[*] fetching columns for table '%s' ..." % args.tbl) # 'entries' phase below streams ROWS, not column names + print("[i] fetching columns for table '%s' ..." % args.tbl) # 'entries' phase below streams ROWS, not column names cols = esp.columns(args.tbl, schema=scope) or None - print("[*] fetching entries for table '%s' ..." % args.tbl) - # NO silent internal 10-row cap: honor --stop, else dump ALL rows (by COUNT) and - # always print whether the result is complete. - if getattr(args, "stop", None): - limit = args.stop - else: - try: - limit = esp.extractInteger("(SELECT COUNT(*) FROM %s)" % esp.qualify(args.tbl, scope)) or 10 - except Exception: - limit = 1 << 30 + print("[i] fetching entries for table '%s' ..." % args.tbl) + # NO silent internal cap: dump ALL rows (bounded by the live COUNT) and always + # print whether the result came back complete. + try: + limit = esp.extractInteger("(SELECT COUNT(*) FROM %s)" % esp.qualify(args.tbl, scope)) or 10 + except Exception: + limit = 1 << 30 result = esp.dump(args.tbl, columns=cols, schema=scope, limit=limit) if not result or not result["columns"]: print("[!] could not dump %s" % args.tbl) @@ -647,6 +640,25 @@ def _report(esp, args): _printTable(result["columns"], result["rows"]) +def _banner(): + # modest CLI identity, sqlmap-styled: a small globe (DBMS-agnostic/universal) + the Esperanto + # green star. Colored only on a TTY; pure-ASCII (no coding header on this file); text columns + # aligned at a fixed offset so the escape codes (zero display width) don't skew them. + import sys as _sys + tty = _sys.stdout.isatty() + G = "\033[0;32m" if tty else "" # esperanto green (the globe) + S = "\033[1;32m" if tty else "" # bright green (the star) + W = "\033[1;37m" if tty else "" # bold white (the name) + U = "\033[4;37m" if tty else "" # underline (the site) + R = "\033[0m" if tty else "" + return ( + " %(G)s___%(R)s\n" + " %(G)s/ _ \\%(R)s %(W)sesperanto%(R)s {%(ver)s}\n" + " %(G)s| (_) |%(R)s\n" + " %(G)s\\___/ %(S)s*%(R)s %(U)s%(site)s%(R)s\n" + ) % dict(G=G, S=S, W=W, U=U, R=R, ver=__version__, site=_SITE) + + def main(argv=None): """Standalone entry point: drive the engine against a live HTTP target.""" import argparse @@ -665,13 +677,14 @@ def _format_action_invocation(self, action): parser = argparse.ArgumentParser( prog="esperanto", formatter_class=_Formatter, + usage="esperanto -u URL [options]", # short synopsis, not an auto-listing of every flag description="DBMS-agnostic blind-SQLi enumeration engine (standalone)") + parser.add_argument("--version", action="version", version="esperanto %s (%s)" % (__version__, _SITE)) parser.add_argument("-u", "--url", help="target URL (with a '*'/'[INFERENCE]' marker)") parser.add_argument("--data", help="POST data string") parser.add_argument("--cookie", help="HTTP Cookie header") parser.add_argument("-H", "--header", action="append", help="extra HTTP header (repeatable)") parser.add_argument("--string", help="match string for a True response") - parser.add_argument("--not-string", dest="not_string", help="match string for a False response") parser.add_argument("--code", type=int, help="HTTP code for a True response") parser.add_argument("--banner", action="store_true", help="retrieve DBMS banner") parser.add_argument("--current-user", action="store_true", dest="current_user", help="retrieve current user") @@ -679,11 +692,9 @@ def _format_action_invocation(self, action): parser.add_argument("--tables", action="store_true", help="enumerate tables") parser.add_argument("--columns", action="store_true", help="enumerate table columns (needs -T)") parser.add_argument("--dump", action="store_true", help="dump table entries (needs -T)") - parser.add_argument("--sql-query", dest="query", help="run a custom scalar SQL query") parser.add_argument("-D", dest="db", help="database/schema to enumerate") parser.add_argument("-T", dest="tbl", help="table to enumerate") parser.add_argument("-C", dest="col", help="columns to dump (comma-separated)") - parser.add_argument("--stop", type=int, help="max rows to dump (default: all)") # internal dev/test harness switches - functional but hidden from --help (--live drives the # local-Docker DBMS livetest; --waf is a livetest-only fault-injection mode, NOT a real WAF bypass) parser.add_argument("--live", action="store_true", help=argparse.SUPPRESS) @@ -697,13 +708,14 @@ def _format_action_invocation(self, action): if args.selftest: # self-test only on EXPLICIT request _selftest() return 0 + print(_banner()) # CLI identity (after the dev-harness paths above) if not args.url: # no target and nothing to do -> show help, don't surprise parser.print_help() return 1 target = args.url if "://" in args.url else ("http://" + args.url) # tolerate a scheme-less URL esp = Esperanto(_httpOracle(target, args.data, args.cookie, args.header, - args.string, args.not_string, args.code)) + args.string, args.code)) import sys as _sys _tty = _sys.stdout.isatty() def _charLive(partial, total): @@ -713,18 +725,18 @@ def _charLive(partial, total): shown = _previewFramed(partial) if shown is None: shown = partial - _sys.stdout.write("\r\033[K[*] retrieved: %s" % _safeterm(shown[-200:])) + _sys.stdout.write("\r\033[K[i] retrieved: %s" % _safeterm(shown[-200:])) if len(partial) >= total: # value complete -> keep it and drop to a new line _sys.stdout.write("\n") _sys.stdout.flush() def _plainLive(value): # piped/non-tty: one plain line per value, no control codes - _sys.stdout.write("[*] retrieved: %s\n" % _safeterm(value)) + _sys.stdout.write("[i] retrieved: %s\n" % _safeterm(value)) _sys.stdout.flush() if _tty: esp._charProgress = _charLive # animated char-by-char is the sole feedback on a terminal else: esp._progress = _plainLive # logs/pipes get clean per-value lines instead - print("[*] discovering the back-end SQL dialect (agnostic mode) ...") + print("[i] discovering the back-end SQL dialect (agnostic mode) ...") try: esp.discover() _report(esp, args) diff --git a/extra/esperanto/enumeration.py b/extra/esperanto/enumeration.py index a295d5f8e55..ef6591efe90 100644 --- a/extra/esperanto/enumeration.py +++ b/extra/esperanto/enumeration.py @@ -329,12 +329,14 @@ def _discoverKey(self, table, schema=None): # take the alphabetically-first key column (deterministic); a compound key # still yields a usable ordering column for the walk keyexpr = "(SELECT MIN(%s) FROM %s WHERE %s)" % (ncol, source, filt) - with self._probePhase(): # a catalog that lacks this key structure errors -> "no key", not fatal - present = self._ask("%s IS NOT NULL" % keyexpr) - if present: - res = self.extractResult(keyexpr) # a key COLUMN NAME becomes SQL -> require an EXACT value - if res.exact and res.value: # (a case-ambiguous name could mis-target) - return res.value + # discovering the key COLUMN NAME is setup, not data - run it inside the probe phase so a + # catalog lacking this key structure degrades to "no key" (not fatal) AND so the name does + # not surface on the live "retrieved:" feed (it is a column name, not a dumped entry). + with self._probePhase(): + if self._ask("%s IS NOT NULL" % keyexpr): + res = self.extractResult(keyexpr) # a key COLUMN NAME becomes SQL -> require an EXACT value + if res.exact and res.value: # (a case-ambiguous name could mis-target) + return res.value return None def columnType(self, expr): diff --git a/lib/core/settings.py b/lib/core/settings.py index 1d65ca73755..8433981815b 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.174" +VERSION = "1.10.7.176" 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)