Fixes #6083
Some checks are pending
/ build (macos-latest, 3.8) (push) Waiting to run
/ build (ubuntu-latest, pypy-2.7) (push) Waiting to run
/ build (windows-latest, 3.14) (push) Waiting to run

This commit is contained in:
Miroslav Štampar 2026-07-13 03:20:07 +02:00
parent 7ee21bbaf9
commit 71afbf0711
3 changed files with 63 additions and 22 deletions

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.103"
VERSION = "1.10.7.104"
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

@ -144,7 +144,7 @@ STATIC_LEN = len(STATIC_TABLE)
# HTTP/2 frame codec (RFC 7540 section 4.1) - the zero-table-risk brick. Pure stdlib, py2/py3, ASCII.
# frame types (RFC 7540 s6)
DATA, HEADERS, RST_STREAM, SETTINGS, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION = 0x0, 0x1, 0x3, 0x4, 0x6, 0x7, 0x8, 0x9
DATA, HEADERS, RST_STREAM, SETTINGS, PUSH_PROMISE, PING, GOAWAY, WINDOW_UPDATE, CONTINUATION = 0x0, 0x1, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9
# flags
FLAG_END_STREAM = 0x1
FLAG_ACK = 0x1
@ -374,6 +374,7 @@ class Encoder(object):
out += encode_string(value)
return bytes(out)
SETTINGS_ENABLE_PUSH = 0x2
SETTINGS_INITIAL_WINDOW_SIZE = 0x4
BIG_WINDOW = (1 << 31) - 1
@ -469,9 +470,12 @@ class _H2Connection(object):
if self.sock.selected_alpn_protocol() != "h2":
raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol())
self.sock.settimeout(timeout)
# connection preface + client SETTINGS (advertise a large per-stream window) + bump conn window
# connection preface + client SETTINGS (disable server push + advertise a large per-stream window)
# + bump conn window. ENABLE_PUSH=0 keeps servers from opening pushed streams whose HPACK header
# block we would otherwise have to decode to keep the dynamic table in sync (skipping it desyncs
# the decoder and corrupts every later header) - this client has no use for pushed responses.
self.sock.sendall(CONNECTION_PREFACE)
self.sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HI", SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW)))
self.sock.sendall(encode_frame(SETTINGS, 0, 0, struct.pack("!HIHI", SETTINGS_ENABLE_PUSH, 0, SETTINGS_INITIAL_WINDOW_SIZE, BIG_WINDOW)))
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", BIG_WINDOW - 65535)))
except Exception:
self.close()
@ -514,6 +518,9 @@ class _H2Connection(object):
elif ftype == PING:
if not (flags & FLAG_ACK):
self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload))
elif ftype == PUSH_PROMISE:
self.usable = False # we advertised ENABLE_PUSH=0; a push would desync HPACK
raise _UnprocessedStream("unexpected PUSH_PROMISE despite SETTINGS_ENABLE_PUSH=0")
elif ftype == GOAWAY:
self.usable = False # server won't accept new streams -> retire connection
last_sid = (struct.unpack("!I", payload[4:8])[0] & 0x7fffffff) if len(payload) >= 8 else 0
@ -601,12 +608,18 @@ class _H2Connection(object):
elif ftype == PING:
if not (flags & FLAG_ACK):
self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload))
elif ftype == PUSH_PROMISE:
self.usable = False # we advertised ENABLE_PUSH=0; a push would desync HPACK
raise _UnprocessedStream("unexpected PUSH_PROMISE despite SETTINGS_ENABLE_PUSH=0")
elif ftype == GOAWAY:
# Routine on a long-lived connection (server retires it after its per-connection request cap).
# The pair did not complete cleanly, so it must be re-sent on a fresh connection; flag it
# retry-safe (idempotent boolean read) rather than crashing the extraction.
self.usable = False
raise IOError("GOAWAY during timeless pair")
raise _UnprocessedStream("GOAWAY during timeless pair")
elif ftype == RST_STREAM and fsid in state:
self.usable = False
raise IOError("stream reset during timeless pair")
raise _UnprocessedStream("stream reset during timeless pair")
elif ftype in (HEADERS, CONTINUATION) and fsid in state:
p = payload
if ftype == HEADERS:

View file

@ -17,6 +17,8 @@ See the file 'LICENSE' for copying permission
# 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 socket
import ssl
import threading
from lib.core.enums import DBMS
@ -49,18 +51,32 @@ def buildConditionPair(condition, heavy, cheap="0"):
return condExpr, negExpr
def _pairOrder(conn, reqA, reqB, timeout):
def _pairOrder(connSource, reqA, reqB, timeout, retries=2):
"""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
stream ids in send order (reqA got the lower id).
`connSource` is either a live _H2Connection or a zero-arg callable returning one. The callable form
lets a dropped connection be replaced transparently and the pair re-sent: a long extraction routinely
outlives a single HTTP/2 connection (the server retires it with GOAWAY after its per-connection request
cap), and a coalesced boolean-read pair is idempotent, so re-sending it on a fresh connection is safe.
A raw connection (used by calibration and the self-test) is not retried - it simply raises."""
attempt = 0
while True:
conn = connSource() if callable(connSource) else connSource
try:
order, _results = conn.exchange_pair([reqA, reqB], timeout)
return order[0], conn.next_sid - 4, conn.next_sid - 2
except (socket.error, ssl.SSLError, IOError):
conn.close() # retire; a callable source reopens on the next pass
attempt += 1
if not callable(connSource) or attempt > retries:
raise
def readBit(conn, reqCond, reqNeg, votes=5, timeout=30):
def readBit(connSource, 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).
ambiguous (load-degraded). `connSource` is a live connection or a factory (see _pairOrder) so a
connection dropped mid-vote (e.g. server GOAWAY) is replaced and that pair re-sent transparently.
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
@ -83,10 +99,10 @@ def readBit(conn, reqCond, reqNeg, votes=5, timeout=30):
cap = votes * 5 # escalation ceiling for ambiguous (load-degraded) bits
while True:
if i % 2 == 0:
first, loSid, _hiSid = _pairOrder(conn, reqCond, reqNeg, timeout)
first, loSid, _hiSid = _pairOrder(connSource, reqCond, reqNeg, timeout)
condSid = loSid
else:
first, _loSid, hiSid = _pairOrder(conn, reqNeg, reqCond, timeout)
first, _loSid, hiSid = _pairOrder(connSource, reqNeg, reqCond, timeout)
condSid = hiSid
if first != condSid: # reqCond finished last -> it ran heavy -> vote says TRUE
condLast += 1
@ -159,6 +175,16 @@ class TimelessOracle(object):
def _conn(self):
conn = getattr(self._local, "conn", None)
if conn is None or not conn.usable:
if conn is not None: # retire the dead one so reconnects don't leak sockets
try:
conn.close()
except Exception:
pass
with self._lock:
try:
self._conns.remove(conn)
except ValueError:
pass
conn = self._local.conn = connect(self.host, self.port, self.proxy, self.timeout)
with self._lock:
self._conns.append(conn)
@ -172,9 +198,11 @@ class TimelessOracle(object):
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)
# Pass the bound _conn factory (not a resolved connection) so a pair whose connection is retired
# mid-read (server GOAWAY on a long dump) is transparently re-sent on a fresh one.
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)
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:
@ -655,7 +683,7 @@ def readBitLive(conn, condition, vector=None, votes=1, timeout=30):
return readBit(conn, reqCond, reqNeg, votes=votes, timeout=timeout)
def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30):
def readBitAsymmetric(connSource, 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
@ -668,10 +696,10 @@ def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30):
condLast = 0
for i in range(votes):
if i % 2 == 0:
order, _ = conn.exchange_pair([reqCond, reqRef], timeout); condSid = conn.next_sid - 4
first, condSid, _hiSid = _pairOrder(connSource, reqCond, reqRef, timeout)
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
first, _loSid, condSid = _pairOrder(connSource, reqRef, reqCond, timeout)
if first != 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%)