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
2 changes: 1 addition & 1 deletion extra/esperanto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion extra/esperanto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__"]
78 changes: 45 additions & 33 deletions extra/esperanto/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (<condition>)`.

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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -456,17 +457,15 @@ 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)
if body is None: # transport failure -> UNDECIDED (tri-state):
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":
Expand Down Expand Up @@ -599,43 +598,37 @@ 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"))
if args.current_db:
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 ["<none>"]))
if args.columns:
if not args.tbl:
print("[!] --columns needs -T <table>")
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 ["<none>"])))
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 <table>")
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)
Expand All @@ -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
Expand All @@ -665,25 +677,24 @@ 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")
parser.add_argument("--current-db", action="store_true", dest="current_db", help="retrieve current database")
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)
Expand All @@ -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):
Expand All @@ -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)
Expand Down
14 changes: 8 additions & 6 deletions extra/esperanto/enumeration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
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)
Expand Down