Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions data/txt/keywords.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1633,3 +1633,5 @@ YEAR

ORD
MID
TOP
ROWNUM
9 changes: 9 additions & 0 deletions lib/controller/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/core/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/core/optiondict.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
"oobServer": "string",
"oobToken": "string",
"timeSec": "integer",
"timeless": "boolean",
"uCols": "string",
"uChar": "string",
"uFrom": "string",
Expand Down
2 changes: 1 addition & 1 deletion lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.34"
VERSION = "1.10.7.38"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
Expand Down
3 changes: 3 additions & 0 deletions lib/parse/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
34 changes: 33 additions & 1 deletion lib/request/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,12 @@ class _(dict):
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

Expand Down Expand Up @@ -1029,7 +1035,7 @@ class _(dict):

@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
Expand All @@ -1039,6 +1045,10 @@ def queryPage(value=None, place=None, content=False, getRatioValue=False, silent
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
Expand Down Expand Up @@ -1515,6 +1525,22 @@ def _randomizeParameter(paramString, randomParameter):
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()
Expand Down Expand Up @@ -1592,6 +1618,12 @@ def _randomizeParameter(paramString, randomParameter):
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)
Expand Down
90 changes: 89 additions & 1 deletion lib/request/http2.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,12 @@ def __init__(self, host, port, proxy, timeout):
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())
Expand Down Expand Up @@ -540,6 +545,89 @@ def exchange(self, method, path, authority, headers, body, timeout):
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.
Expand Down
Loading