diff --git a/extra/esperanto/README.md b/extra/esperanto/README.md index a66e387f5..a22ee07ec 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 45e5fa09e..fd7121ddb 100644 --- a/extra/esperanto/__init__.py +++ b/extra/esperanto/__init__.py @@ -19,10 +19,12 @@ 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. """ +__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 ea6f116d5..1ee6e02f2 100644 --- a/extra/esperanto/__main__.py +++ b/extra/esperanto/__main__.py @@ -5,11 +5,14 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) 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 _livetest(only=None, waf=False): 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 @@ -428,8 +431,6 @@ def _httpOracle(url, data=None, cookie=None, headers=None, string=None, notStrin 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,7 +457,7 @@ def _httpOracle(url, data=None, cookie=None, headers=None, string=None, notStrin break if wanted is None: # (3) similarity ratio floor mode, base = "ratio", {"t": tc, "f": fc} - if not string and not notString: + if string is None: print("[*] calibrated oracle: %s%s" % (mode, (" (%r)" % wanted) if mode in ("code", "autostring") else "")) def classify(cond): @@ -465,8 +466,6 @@ def _httpOracle(url, data=None, cookie=None, headers=None, string=None, notStrin 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": @@ -647,6 +646,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 +683,14 @@ def main(argv=None): 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") @@ -697,13 +716,14 @@ def main(argv=None): 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): diff --git a/lib/core/settings.py b/lib/core/settings.py index 1d65ca737..e72cf91e2 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.174" +VERSION = "1.10.7.175" 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)