diff --git a/lib/core/settings.py b/lib/core/settings.py index 769d18cb3..e96e15674 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.161" +VERSION = "1.10.7.162" 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) diff --git a/lib/request/connect.py b/lib/request/connect.py index fb57614fa..fc23cf99a 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -1572,6 +1572,7 @@ class Connect(object): # 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 CONNECTIVITY_ERRORS 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 @@ -1580,7 +1581,17 @@ class Connect(object): 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) + try: + return kb.timeless.readBitFromSpecs(condSpec, negSpec) + except CONNECTIVITY_ERRORS as ex: + # The oracle's own per-pair retries (see _pairOrder) are exhausted - the target has stopped + # negotiating HTTP/2 altogether (e.g. a load-balanced backend that only some nodes speak it + # on), not just dropped one connection. Disengage (restores the classic time-based vector) + # and fall through below to the normal wall-clock comparison instead of crashing the scan. + from lib.request.timeless import disengage + warnMsg = "HTTP/2 timeless timing lost connectivity ('%s'). Falling back to classic time-based" % getSafeExString(ex) + singleTimeWarnMessage(warnMsg) + disengage() if timeBasedCompare and not conf.disableStats: if len(kb.responseTimes.get(kb.responseTimeMode, [])) < MIN_TIME_RESPONSES: diff --git a/lib/request/timeless.py b/lib/request/timeless.py index f0997594e..619ddfc34 100644 --- a/lib/request/timeless.py +++ b/lib/request/timeless.py @@ -27,6 +27,12 @@ from lib.request.http2 import _H2Connection # Serializes the one-shot autoEngage() so concurrent worker threads never double-calibrate/double-engage. _engageLock = threading.Lock() +# Transport-level failures that mean "this connection/attempt is unusable" - covers a mid-exchange drop +# (GOAWAY, reset) as well as a failed (re)connect, including the h2 client's own IOError when the server +# does not negotiate ALPN 'h2' on a fresh socket (seen on backends that speak h2 inconsistently, e.g. only +# some nodes behind a load balancer). Shared by _pairOrder (per-pair retry) and connect.py (the give-up path). +CONNECTIVITY_ERRORS = (socket.error, ssl.SSLError, IOError) + def buildConditionPair(condition, heavy, cheap="0"): """Turn a boolean `condition` (the same comparison bisection injects at INFERENCE_MARKER, e.g. @@ -59,15 +65,19 @@ def _pairOrder(connSource, reqA, reqB, timeout, retries=2): 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.""" + Opening that fresh connection is retried the same way - it can fail just like an in-progress exchange + (including ALPN renegotiation failing on the new socket). 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 + conn = None try: + conn = connSource() if callable(connSource) else connSource 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 + except CONNECTIVITY_ERRORS: + if conn is not None: + conn.close() # retire; a callable source reopens on the next pass attempt += 1 if not callable(connSource) or attempt > retries: raise