From 2626e501904f03e985eeebfad152ee7a577ba73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Mon, 20 Jul 2026 17:19:28 +0200 Subject: [PATCH] Adding boundarycheck tool --- extra/boundarycheck/README.md | 112 ++++++++ extra/boundarycheck/boundarycheck.py | 389 +++++++++++++++++++++++++++ extra/boundarycheck/run.sh | 43 +++ lib/core/settings.py | 2 +- 4 files changed, 545 insertions(+), 1 deletion(-) create mode 100644 extra/boundarycheck/README.md create mode 100644 extra/boundarycheck/boundarycheck.py create mode 100644 extra/boundarycheck/run.sh diff --git a/extra/boundarycheck/README.md b/extra/boundarycheck/README.md new file mode 100644 index 000000000..340de757f --- /dev/null +++ b/extra/boundarycheck/README.md @@ -0,0 +1,112 @@ +# boundarycheck + +A live cross-DBMS validator for `data/xml/boundaries.xml`. For every reachable DBMS it builds the +**real** injected value for each boundary (through sqlmap's own `agent.prefixQuery` / `suffixQuery` / +`cleanupPayload`, not a re-implementation) across representative host-query contexts, runs it against +the engine, and classifies each boundary via three oracles: + +``` +W boolean TRUE/FALSE variants are valid AND their row counts differ +T time an inline conditional sleep delays on TRUE, not on FALSE (MySQL / PostgreSQL) +I inband an injected marker surfaces in the result set (table cross-join / UNION) +. inert valid SQL but no channel discriminated in the contexts tried +x invalid a syntax / semantic error on this engine +``` + +It prints a `boundary x engine` matrix plus the boundaries usable via **no** oracle on any engine +(the "no working context found" review candidates). Any engine that does not connect is **skipped** +and reported as such — never silently counted as covered. + +> Scope: covers boolean + inline-time (MySQL/PG) + inband over a sampled set of contexts. It does +> **not** model an error-based oracle, inline time on MSSQL/Oracle (statement/privilege-gated sleep), +> or every possible host context. So a boundary flagged "usable via no oracle" may still work via one +> of those — confirm before acting. + +--- + +## Quick start (throwaway MySQL + PostgreSQL) + +```bash +pip install pymysql psycopg2-binary # python drivers (see below for MSSQL/Oracle) +./run.sh # spins up 2 containers, runs, tears everything down +``` + +`run.sh` is fully disposable: it creates two containers, waits for readiness, runs the tool, and +removes them again on exit (including Ctrl-C / failure). Nothing persists. + +--- + +## Full lab (all four engines) — step by step + +The tool's built-in default endpoints (override any via env var — see below): + +| engine | host:port | user / pass | db / service | +|------------|-------------------|----------------------|--------------| +| MySQL | `127.0.0.1:13306` | `root` / `root` | — | +| PostgreSQL | `127.0.0.1:15432` | `esp` / `pass` | `espdb` | +| MSSQL | `127.0.0.1:11433` | `sa` / `Esp_pass123` | — | +| Oracle | `127.0.0.1:1521` | `system` / `oracle` | `FREEPDB1` | + +### 1. Python drivers + +```bash +pip install pymysql psycopg2-binary pymssql oracledb +``` + +(Install only the ones you need — a missing driver just skips that engine.) + +### 2. Start the containers + +```bash +# MySQL (fast) +docker run -d --rm --name bcheck-mysql -e MYSQL_ROOT_PASSWORD=root -p 13306:3306 mysql:8.4 + +# PostgreSQL (fast) +docker run -d --rm --name bcheck-pg -e POSTGRES_USER=esp -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=espdb -p 15432:5432 postgres:16 + +# MSSQL (slower; ~30-60s to accept connections) +docker run -d --rm --name bcheck-mssql -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=Esp_pass123 -p 11433:1433 mcr.microsoft.com/mssql/server:2022-latest + +# Oracle Free (slowest; first boot can take a few minutes and the image is large) +docker run -d --rm --name bcheck-oracle -e ORACLE_PASSWORD=oracle -p 1521:1521 gvenzl/oracle-free:slim +``` + +Give MSSQL/Oracle time to finish initialising before running the tool (watch `docker logs -f `). + +### 3. Run + +```bash +python3 boundarycheck.py # from anywhere; it locates the sqlmap root itself +``` + +### 4. Override an endpoint (optional) + +``` +BC_MYSQL=host:port:user:pass +BC_POSTGRES=host:port:user:pass:db +BC_MSSQL=host:port:user:pass +BC_ORACLE=host:port:user:pass:service # service_name goes in the db slot +``` + +```bash +BC_MYSQL=10.0.0.5:3306:root:secret python3 boundarycheck.py +``` + +### 5. Tear down + +```bash +docker rm -f bcheck-mysql bcheck-pg bcheck-mssql bcheck-oracle +``` + +--- + +## Notes + +- **Faithfulness:** the tool imports the `plugins.dbms.*` packages so each DBMS's `unescaper` is + registered. Without that, `SELECT '[RANDSTR]'` renders as a bare identifier instead of the real + `0x`-hex / `CHR()` literal and concat-style boundaries are mis-reported. A `_faithful_or_die()` + self-check aborts if the registration ever fails to load. +- **Side-effect-free on the target data:** it creates a scratch table `bc` (and a `boundarycheck` + database on MySQL), and drops them on completion. DML boundaries write only to a spare `note` column. +- Engines are used purely as SQL oracles over localhost throwaway containers — do not point the + override env vars at anything you care about. diff --git a/extra/boundarycheck/boundarycheck.py b/extra/boundarycheck/boundarycheck.py new file mode 100644 index 000000000..17de09c19 --- /dev/null +++ b/extra/boundarycheck/boundarycheck.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Live cross-DBMS validator for data/xml/boundaries.xml. + +WHAT + For every reachable DBMS this builds the REAL injected parameter value for each + boundary - through the actual agent.prefixQuery / suffixQuery / cleanupPayload + path, NOT a re-implementation - across a set of representative host-query + contexts, runs the TRUE and FALSE variants against the live engine, and + classifies the boundary via three oracles: + + W (WORKS) boolean: TRUE/FALSE variants are valid AND their row counts differ + T (TIME) inline conditional sleep delays on TRUE, not on FALSE (MySQL/PG only) + I (INBAND) an injected marker surfaces in the result set (table cross-join / UNION) + . (inert) valid SQL but no channel discriminates in the contexts tried + x (invalid) a syntax/semantic error on this engine + + Output is a boundary x engine matrix plus the boundaries usable via NO oracle on any + reachable engine - the "no working context found" review candidates. + +WHY + boundaries.xml is the detection core. This answers "does boundary X actually + produce valid, discriminating SQL on engine Y" with evidence instead of argument. + +FAITHFULNESS (do not remove the plugins.dbms.* imports) + The per-DBMS `unescaper` registrations load only when the plugin packages are + imported. Without them "SELECT '[RANDSTR]'" renders as a bare identifier instead + of the real 0x-hex / CHR() literal, and every concat-style boundary is falsely + reported broken. The import plus _faithful_or_die() guard against that. + +SCOPE / LIMITATION + Covers the boolean, inline-time (MySQL/PG) and inband oracles over a representative + set of host contexts. Still NOT modeled: the error-based oracle, inline time on + MSSQL/Oracle (statement/privilege-gated sleep), and any host context not in the set. + So a boundary flagged "usable via NO oracle" may still work via one of those - confirm + before acting. "inert" means no channel discriminated in the contexts tried. + +BENCH + Needs live engines. Defaults target the local docker bench; any engine that does + not connect (driver missing or refused) is SKIPPED and reported as skipped - never + silently counted as covered. Override a target with an env var (host:port:user:pass[:db]): + + MySQL BC_MYSQL default 127.0.0.1:13306:root:root + PostgreSQL BC_POSTGRES default 127.0.0.1:15432:esp:pass:espdb + MSSQL BC_MSSQL default 127.0.0.1:11433:sa:Esp_pass123 + Oracle BC_ORACLE default 127.0.0.1:1521:system:oracle:FREEPDB1 (service_name in db slot) + +USAGE (from the sqlmap root) + python extra/boundarycheck/boundarycheck.py +""" + +from __future__ import print_function + +import os +import sys +import time +import xml.etree.ElementTree as ET + +_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, _ROOT) +sys.path.insert(0, os.path.join(_ROOT, "tests")) + +from _testutils import bootstrap, set_dbms +bootstrap() + +# Faithfulness (see FAITHFULNESS above): importing each DBMS plugin package runs its module-level +# `unescaper[] = Syntax.escape` registration - a side effect, not a symbol we use. Trigger it via +# import_module so there is no bound-but-unused name for pyflakes to flag. +import importlib +for _plugin in ("mysql", "postgresql", "oracle", "mssqlserver"): + importlib.import_module("plugins.dbms.%s" % _plugin) + +from lib.core.data import conf, kb +from lib.core.datatype import AttribDict +from lib.core.enums import PAYLOAD +from lib.core.unescaper import unescaper +import lib.core.agent as _agentmod + +agent = _agentmod.agent +conf.noEscape = False + +TRUE_PAYLOAD = "AND 9911=9911" +FALSE_PAYLOAD = "AND 9911=9912" +TIME_THRESHOLD = 0.4 # seconds; a conditional sleep of ~0.7s must clear this, a no-op must not +INBAND_MARKER = "qBCiNBANDq" # distinctive token; if it surfaces in the result set the +INBAND_PAYLOAD = "(SELECT '%s')" % INBAND_MARKER # boundary opens an inband output channel (e.g. a table cross-join) + +# per-engine conditional-sleep payload pair (delay-if-true, no-delay) for the TIME oracle. Only engines +# with an INLINE sleep expression are covered; MSSQL (WAITFOR is a statement) and Oracle (DBMS_LOCK/PIPE, +# privilege-gated) need a statement/stacked sleep, so their inline time-usability is NOT modeled here. +SLEEP = { + "MySQL": ("AND 0=(SELECT SLEEP(0.7))", "AND 0=(SELECT SLEEP(0))"), # subquery => fires once, not per-row + "PostgreSQL": ("AND 1=(SELECT 1 FROM pg_sleep(0.7))", "AND 1=(SELECT 1 FROM pg_sleep(0))"), +} + +# portable WHERE-clause contexts at parenthesis depths 0-3 (numeric + single-quoted string), so the +# ')' / '))' / ')))' and "'" / "')" / "'))" boundary families each meet a host that actually closes +# the matching depth. Reused by every engine; engine-specific contexts are appended in the adapters. +WHERE_CONTEXTS = [ + ("SELECT COUNT(*) FROM bc WHERE name LIKE '%%%s%%'", "orig"), # LIKE '%...%' + ("SELECT COUNT(*) FROM bc %s", ""), # bare pre-WHERE (add a WHERE) + ("SELECT COUNT(*) FROM (SELECT * FROM bc WHERE id=%s) q", "1"), # derived table (numeric) + ("SELECT COUNT(*) FROM (SELECT * FROM bc WHERE name='%s') q", "orig"), # derived table (single-quote) + ("SELECT COUNT(*) FROM (SELECT * FROM (SELECT * FROM bc WHERE id=%s) a) q", "1"), # derived table 2-deep + ("SELECT COUNT(*) FROM (SELECT * FROM (SELECT * FROM bc WHERE name='%s') a) q", "orig"), + ("UPDATE bc SET note='%s' WHERE id=1", "orig"), # pre-WHERE DML (rowcount oracle) + ("SELECT * FROM %s", "bc"), # table-name (inband cross-join) +] +for _depth in range(4): + _open, _close = "(" * _depth, ")" * _depth + WHERE_CONTEXTS.append(("SELECT COUNT(*) FROM bc WHERE " + _open + "id=%s" + _close, "1")) + WHERE_CONTEXTS.append(("SELECT COUNT(*) FROM bc WHERE " + _open + "name='%s'" + _close, "orig")) + + +def _faithful_or_die(): + set_dbms("MySQL") + if unescaper.escape("abc", quote=False) == "abc": + sys.exit("FATAL: unescaper not registered - '[RANDSTR]' would render as a bare identifier and " + "verdicts would be wrong. Check the plugins.dbms.* imports at the top of this file.") + + +def build_value(dbms, prefix, suffix, clause, payload, orig): + """The real injected parameter value for where=ORIGINAL (checks.py:490-493 + agent.payload line 181/183).""" + set_dbms(dbms) + kb.injection = AttribDict() + for _ in ("prefix", "suffix", "clause", "ptype", "place", "parameter"): + kb.injection[_] = None + kb.injection.data = AttribDict() + kb.technique = None + forged = agent.prefixQuery(agent.cleanupPayload(payload), prefix, PAYLOAD.WHERE.ORIGINAL, clause) + forged = agent.suffixQuery(forged, None, suffix, PAYLOAD.WHERE.ORIGINAL) + value = agent.cleanupPayload("%s%s" % (orig, forged), orig) or "" + return value.replace(_agentmod.BOUNDARY_BACKSLASH_MARKER, "\\") + + +def make_run(cur, rollback=None): + """A query runner: returns ("ok", rows) for a result set, ("ok", ("rc", n)) for a DML row count + (cursor.description is None => no result set), or ("err", msg). DML lets pre-WHERE boundaries be + judged by affected-row count (TRUE matches rows, FALSE matches none).""" + def run(sql): + try: + cur.execute(sql) + except Exception as e: + if rollback: + try: + rollback() + except Exception: + pass + return ("err", str(e).splitlines()[-1][:50]) + if cur.description is None: + return ("ok", ("rc", cur.rowcount)) + return ("ok", tuple(cur.fetchall())) + return run + + +def classify(run, dbms, prefix, suffix, clause, contexts): + """Best verdict of a boundary across the engine's contexts: WORKS(boolean) > TIME > inert > invalid. + + The TIME oracle only runs when the boolean probe was valid-but-inert (best=='inert') - i.e. the + injected SQL parses and runs but the boolean AND does not change the row count. That is exactly the + error/time-primitive candidate: a conditional sleep that delays on TRUE and not on FALSE proves the + injected expression actually executes. If every boolean probe was a syntax error (best=='invalid'), + the same syntax fails the time payload too, so it is skipped.""" + best = "invalid" + for host, orig in contexts: + st, rt = run(host % build_value(dbms, prefix, suffix, clause, TRUE_PAYLOAD, orig)) + sf, rf = run(host % build_value(dbms, prefix, suffix, clause, FALSE_PAYLOAD, orig)) + if st == "ok" and sf == "ok": + if rt != rf: + return "WORKS" + best = "inert" + + if best == "inert" and dbms in SLEEP: + slow_p, fast_p = SLEEP[dbms] + for host, orig in contexts: + t0 = time.time() + run(host % build_value(dbms, prefix, suffix, clause, slow_p, orig)) + if time.time() - t0 >= TIME_THRESHOLD: # TRUE variant delayed + t0 = time.time() + run(host % build_value(dbms, prefix, suffix, clause, fast_p, orig)) + if time.time() - t0 < TIME_THRESHOLD: # FALSE variant did not + return "TIME" + + # inband: the injected expression's output surfaces directly in the result set (a table-name + # cross-join, UNION or select-list channel) - invisible to the boolean/time oracles above. + for host, orig in contexts: + st, r = run(host % build_value(dbms, prefix, suffix, clause, INBAND_PAYLOAD, orig)) + if st == "ok" and INBAND_MARKER in str(r): + return "INBAND" + return best + + +def _env(name, default): + parts = (os.environ.get(name) or default).split(":") + parts += [None] * (5 - len(parts)) + host, port, user, pwd, db = parts[:5] + return host, int(port), user, pwd, db + + +# --- engine adapters: return (sqlmap_dbms, run_fn, contexts, cleanup_fn) or None to skip ------------- + +def _mysql(): + try: + import pymysql + h, p, u, w, _ = _env("BC_MYSQL", "127.0.0.1:13306:root:root") + c = pymysql.connect(host=h, port=p, user=u, password=w, connect_timeout=5, autocommit=True) + cur = c.cursor() + cur.execute("CREATE DATABASE IF NOT EXISTS boundarycheck") + cur.execute("USE boundarycheck") + for q in ("DROP TABLE IF EXISTS bc", + "CREATE TABLE bc(id INT,name VARCHAR(64),note VARCHAR(64),FULLTEXT(name)) ENGINE=InnoDB", + "INSERT INTO bc VALUES(1,'orig','n'),(2,'x','n'),(3,'y','n')"): + cur.execute(q) + c.commit() + except Exception as ex: + return None, str(ex).splitlines()[0][:60] + + run = make_run(cur, c.rollback) + contexts = WHERE_CONTEXTS + [ + ("SELECT `%s` FROM bc", "name"), # backtick column identifier + ('SELECT COUNT(*) FROM bc WHERE name="%s"', "orig"), # MySQL: " is a string delim + ('SELECT COUNT(*) FROM bc WHERE (name="%s")', "orig"), + ('SELECT COUNT(*) FROM bc WHERE ((name="%s"))', "orig"), + ('SELECT COUNT(*) FROM bc WHERE (((name="%s")))', "orig"), + ('UPDATE bc SET note="%s" WHERE id=1', "orig"), # double-quote pre-WHERE DML + ('SELECT COUNT(*) FROM (SELECT * FROM bc WHERE name="%s") q', "orig"), # dq derived table + ('SELECT COUNT(*) FROM (SELECT * FROM (SELECT * FROM bc WHERE name="%s") a) q', "orig"), + ("SELECT COUNT(*) FROM bc WHERE id=1 /* c='%s' */", "x"), # block comment + ("SELECT COUNT(*) FROM bc WHERE MATCH(name) AGAINST('%s')", "orig"), # fulltext (IN BOOLEAN MODE) + ("SELECT COUNT(*) FROM `%s`", "bc"), # backtick table + ("SELECT COUNT(*) FROM (SELECT * FROM `%s`) q", "bc")] # backtick table in derived + + def cleanup(): + try: + cur.execute("DROP DATABASE boundarycheck"); c.commit(); c.close() + except Exception: + pass + return ("MySQL", run, contexts, cleanup), None + + +def _postgres(): + try: + import psycopg2 + h, p, u, w, db = _env("BC_POSTGRES", "127.0.0.1:15432:esp:pass:espdb") + c = psycopg2.connect(host=h, port=p, user=u, password=w, dbname=db, connect_timeout=5) + c.autocommit = True + cur = c.cursor() + cur.execute("DROP TABLE IF EXISTS bc") + cur.execute("CREATE TABLE bc(id INT,name VARCHAR(64),note VARCHAR(64))") + cur.execute("INSERT INTO bc VALUES(1,'orig','n'),(2,'x','n'),(3,'y','n')") + except Exception as ex: + return None, str(ex).splitlines()[0][:60] + + run = make_run(cur, c.rollback) + contexts = WHERE_CONTEXTS + [ + ('SELECT id FROM bc ORDER BY "%s"', "name"), # ANSI double-quote identifier + ("SELECT COUNT(*) FROM bc WHERE id=1 /* c='%s' */", "x"), # block comment + ("SELECT COUNT(*) FROM bc WHERE name=$$%s$$", "orig")] # dollar quoting + + def cleanup(): + try: + cur.execute("DROP TABLE bc"); c.close() + except Exception: + pass + return ("PostgreSQL", run, contexts, cleanup), None + + +def _mssql(): + try: + import pymssql + h, p, u, w, _ = _env("BC_MSSQL", "127.0.0.1:11433:sa:Esp_pass123") + c = pymssql.connect(server=h, port=p, user=u, password=w, database="master", autocommit=True, login_timeout=5) + cur = c.cursor() + for q in ("IF OBJECT_ID('bc') IS NOT NULL DROP TABLE bc", "CREATE TABLE bc(id INT,name VARCHAR(64),note VARCHAR(64))", + "INSERT INTO bc VALUES(1,'orig','n'),(2,'x','n'),(3,'y','n')"): + cur.execute(q) + except Exception as ex: + return None, str(ex).splitlines()[-1][:60] + + run = make_run(cur) + contexts = WHERE_CONTEXTS + [ + ("SELECT [%s] FROM bc", "name"), # bracket column identifier (string) + ("SELECT [%s] FROM bc", "id"), # bracket column identifier (numeric) + ("SELECT COUNT(*) FROM bc WHERE id=1 /* c='%s' */", "x")] # block comment + + def cleanup(): + try: + cur.execute("IF OBJECT_ID('bc') IS NOT NULL DROP TABLE bc"); c.close() + except Exception: + pass + return ("Microsoft SQL Server", run, contexts, cleanup), None + + +def _oracle(): + try: + try: + import oracledb as ora + except ImportError: + import cx_Oracle as ora + h, p, u, w, svc = _env("BC_ORACLE", "127.0.0.1:1521:system:oracle:FREEPDB1") + c = ora.connect(user=u, password=w, dsn=ora.makedsn(h, p, service_name=svc)) + c.autocommit = True + cur = c.cursor() + for q in ("BEGIN EXECUTE IMMEDIATE 'DROP TABLE bc'; EXCEPTION WHEN OTHERS THEN NULL; END;", + "CREATE TABLE bc(id INT, name VARCHAR2(64), note VARCHAR2(64))", + "INSERT INTO bc VALUES(1,'orig','n')", "INSERT INTO bc VALUES(2,'x','n')"): + cur.execute(q) + c.commit() + except Exception as ex: + return None, str(ex).splitlines()[0][:60] + + run = make_run(cur, c.rollback) + contexts = WHERE_CONTEXTS + [ + ("SELECT COUNT(*) FROM bc WHERE id=1 /* c='%s' */", "x"), # block comment + ("SELECT COUNT(*) FROM bc WHERE name=q'[%s]'", "orig"), # alternative quoting q'[...]' + ("SELECT COUNT(*) FROM bc WHERE name=q'{%s}'", "orig"), + ("SELECT COUNT(*) FROM bc WHERE name=q'(%s)'", "orig"), + ("SELECT COUNT(*) FROM bc WHERE name=q'<%s>'", "orig")] + + def cleanup(): + try: + c.close() + except Exception: + pass + return ("Oracle", run, contexts, cleanup), None + + +ADAPTERS = [_mysql, _postgres, _mssql, _oracle] + + +def main(): + _faithful_or_die() + + boundaries = ET.parse(os.path.join(_ROOT, "data", "xml", "boundaries.xml")).findall(".//boundary") + + engines, skipped = [], [] + for adapter in ADAPTERS: + got, why = adapter() + (engines if got else skipped).append(got or (adapter.__name__.strip("_"), why)) + + if not engines: + print("no reachable DBMS - nothing validated. Bring up the bench (see module docstring).") + for name, why in skipped: + print(" skipped %-12s (%s)" % (name, why)) + return + + names = [e[0] for e in engines] + mark = {"WORKS": "W", "TIME": "T", "INBAND": "I", "inert": ".", "invalid": "x"} + print("boundary%s %s" % (" " * 38, " ".join("%-6s" % n[:6] for n in names))) + print("-" * (46 + 8 * len(names))) + + verdicts = [] + for idx, b in enumerate(boundaries, 1): + g = lambda k: (b.findtext(k) or "").strip() + prefix, suffix = b.findtext("prefix") or "", b.findtext("suffix") or "" # NOT stripped: leading/trailing space is significant SQL + clause = [int(x) for x in (g("clause") or "0").split(",") if x.strip().isdigit()] + row = [] + for dbms, run, contexts, _ in engines: + row.append(classify(run, dbms, prefix, suffix, clause, contexts)) + verdicts.append((idx, prefix, row)) + cells = " ".join("%-6s" % mark[v] for v in row) + print("#%02d p%s c%-5s %-30r %s" % (idx, g("ptype"), g("clause"), prefix[:30], cells)) + + for _, _, _, cleanup in engines: + cleanup() + + print("\nlegend: W=works (boolean) T=works (time) I=works (inband output) .=valid but inert x=invalid") + print("contexts modeled: WHERE numeric/single-quote (paren depths 0-3), LIKE, block comment, plus") + print(" per-engine (MySQL: double-quote + backtick table; PG: ANSI-ident + dollar; Oracle: q'..').") + print(" NOT modeled: pre-WHERE DML (clause 9), derived-table AS-alias, table/column identifier,") + print(" fulltext AGAINST - boundaries needing those (and any error/time-only ones) show inert here.") + print("engines: " + ", ".join(names)) + for name, why in skipped: + print("skipped: %-14s (%s)" % (name, why)) + + never = [idx for idx, _, row in verdicts if not ({"WORKS","TIME","INBAND"} & set(row))] + print("\nboundaries usable via NO oracle (boolean/time/inband) on any reachable engine (review): %s" + % (", ".join("#%02d" % i for i in never) or "none")) + print("NOTE: boolean + inline-time oracles over sampled contexts. Still unmodeled: the ERROR oracle,") + print(" inline time on MSSQL/Oracle (statement/privilege-gated sleep), and some host contexts.") + print(" A boundary here may still be usable via one of those, so confirm before acting.") + + +if __name__ == "__main__": + main() diff --git a/extra/boundarycheck/run.sh b/extra/boundarycheck/run.sh new file mode 100644 index 000000000..745fb22e0 --- /dev/null +++ b/extra/boundarycheck/run.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# Disposable boundary-check lab. Spins up throwaway MySQL + PostgreSQL containers, runs the +# validator against them, and tears everything down again (even on Ctrl-C / failure). +# +# It uses uncommon host ports and points the tool at them via BC_* env vars, so it will NOT collide +# with any DBMS you already have running (including sqlmap's usual bench). These two engines start in +# seconds and exercise the boolean, time and inband oracles plus the MySQL-only (backtick, double- +# quote, fulltext) and PostgreSQL-only (dollar-quote) families. MSSQL and Oracle are slower/heavier - +# see README.md to add them for full coverage; the tool reports absent engines as "skipped". +# +# Usage: ./run.sh (requires docker + python drivers pymysql, psycopg2 - see README.md) +# +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +MYSQL_PORT=13399 +PG_PORT=15499 + +cleanup() { docker rm -f bcheck-mysql bcheck-pg >/dev/null 2>&1 || true; } +trap cleanup EXIT +cleanup # clear any stale containers from a previous aborted run + +echo "[*] starting throwaway MySQL (:$MYSQL_PORT) and PostgreSQL (:$PG_PORT) ..." +docker run -d --rm --name bcheck-mysql -e MYSQL_ROOT_PASSWORD=root -p ${MYSQL_PORT}:3306 mysql:8.4 >/dev/null +docker run -d --rm --name bcheck-pg -e POSTGRES_USER=esp -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=espdb -p ${PG_PORT}:5432 postgres:16 >/dev/null + +echo "[*] waiting for readiness ..." +# a real query, not just ping: the mysql:8.4 image accepts pings mid-init then restarts once +for _ in $(seq 1 90); do docker exec bcheck-mysql mysql -uroot -proot -e "SELECT 1" >/dev/null 2>&1 && break; sleep 2; done +for _ in $(seq 1 60); do docker exec bcheck-pg pg_isready -U esp >/dev/null 2>&1 && break; sleep 1; done + +echo "[*] running boundarycheck ..." +# only touch the throwaway containers we just created; force MSSQL/Oracle to skip (closed ports) so +# this never connects to - and creates/drops tables on - whatever might be listening on their defaults. +BC_MYSQL="127.0.0.1:${MYSQL_PORT}:root:root" \ +BC_POSTGRES="127.0.0.1:${PG_PORT}:esp:pass:espdb" \ +BC_MSSQL="127.0.0.1:59998:x:x" \ +BC_ORACLE="127.0.0.1:59997:x:x:x" \ +python3 "$HERE/boundarycheck.py" + +echo "[*] done. MSSQL/Oracle were not started here, so their engine-specific boundaries show in the" +echo " review list - add them per README.md for full coverage. Tearing down (trap removes containers)." diff --git a/lib/core/settings.py b/lib/core/settings.py index 96e577123..e2a9a0058 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.152" +VERSION = "1.10.7.153" 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)