From d6b491dec4eee9277db89133adc0a1f6e7873ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Wed, 1 Jul 2026 00:00:47 +0200 Subject: [PATCH] Minor safety mechanism for HEAD null connection --- data/txt/sha256sums.txt | 4 +- lib/controller/checks.py | 89 ++++++++++++++++++++++++++++------------ lib/core/settings.py | 11 ++++- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/data/txt/sha256sums.txt b/data/txt/sha256sums.txt index 0736b248e..76a24f6bd 100644 --- a/data/txt/sha256sums.txt +++ b/data/txt/sha256sums.txt @@ -162,7 +162,7 @@ df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/ 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py 617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py -d6d9159d00f47995cb7414a9e0be1dd088b584ef7ce1eeeb2c9008dec3363e5f lib/controller/checks.py +6f3198df20330b6ff0eb7f615082ca7046e6887ac5d3e5df3598d36f66724e01 lib/controller/checks.py 666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py 1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py @@ -189,7 +189,7 @@ f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decor 9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py 0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py 888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py -c6e83cef57c4b6d492cf3de91ea3b3b176971c36c773759737b6c95269cfadf9 lib/core/settings.py +4d9cc21e2b2a10fd6c06ce6c9b248fd16a4c266511cd01156bbe7643e5327a89 lib/core/settings.py c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py 19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py diff --git a/lib/controller/checks.py b/lib/controller/checks.py index aea85795f..95417492c 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -93,6 +93,8 @@ from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH from lib.core.settings import MAX_STABILITY_DELAY from lib.core.settings import NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH from lib.core.settings import NOSQL_ERROR_REGEX +from lib.core.settings import NULL_CONNECTION_LENGTH_TOLERANCE_HIGH +from lib.core.settings import NULL_CONNECTION_LENGTH_TOLERANCE_LOW from lib.core.settings import NULL_CONNECTION_SKIP_READ_MIN_LENGTH from lib.core.settings import PRECONNECT_INCOMPATIBLE_SERVERS from lib.core.settings import SINGLE_QUOTE_MARKER @@ -1533,44 +1535,79 @@ def checkNullConnection(): pushValue(kb.pageCompress) kb.pageCompress = False + # A method is accepted only if the length it reports tracks the real GET response. The + # original page length (len(kb.originalPage)) is the reference; a method whose length is + # grossly off (e.g. HEAD returning 'Content-Length: 0', HEAD served from a different code + # path, or sneaked-in compression) would otherwise make every page look identical and + # silently break detection. The band is coarse on purpose (byte-vs-character size and + # moderate page dynamism are expected); a false reject just forgoes the optimization + def _plausibleLength(length): + reference = len(kb.originalPage or "") + if not reference: + return True + return NULL_CONNECTION_LENGTH_TOLERANCE_LOW * reference <= length <= NULL_CONNECTION_LENGTH_TOLERANCE_HIGH * reference + try: page, headers, _ = Request.getPage(method=HTTPMETHOD.HEAD, raise404=False) if not page and HTTP_HEADER.CONTENT_LENGTH in (headers or {}): - kb.nullConnection = NULLCONNECTION.HEAD + try: + length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) + except ValueError: + length = None - infoMsg = "NULL connection is supported with HEAD method ('Content-Length')" - logger.info(infoMsg) - else: + if length is not None and _plausibleLength(length): + kb.nullConnection = NULLCONNECTION.HEAD + + infoMsg = "NULL connection is supported with HEAD method ('Content-Length')" + logger.info(infoMsg) + elif length is not None: + debugMsg = "HEAD method reports an implausible 'Content-Length' (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) + + if kb.nullConnection is None: page, headers, _ = Request.getPage(auxHeaders={HTTP_HEADER.RANGE: "bytes=-1"}) if page and len(page) == 1 and HTTP_HEADER.CONTENT_RANGE in (headers or {}): - kb.nullConnection = NULLCONNECTION.RANGE + try: + length = int(headers[HTTP_HEADER.CONTENT_RANGE][headers[HTTP_HEADER.CONTENT_RANGE].find('/') + 1:]) + except ValueError: + length = None - infoMsg = "NULL connection is supported with GET method ('Range')" - logger.info(infoMsg) - else: - _, headers, _ = Request.getPage(skipRead=True) + if length is not None and _plausibleLength(length): + kb.nullConnection = NULLCONNECTION.RANGE - if HTTP_HEADER.CONTENT_LENGTH in (headers or {}): - try: - length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) - except ValueError: - length = len(kb.originalPage or "") + infoMsg = "NULL connection is supported with GET method ('Range')" + logger.info(infoMsg) + elif length is not None: + debugMsg = "'Range' method reports an implausible total length (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) - # Unlike HEAD/Range, 'skip-read' leaves the body unread and must close the - # connection (an unread body cannot be reused), paying a fresh TCP/TLS handshake - # per request. That only outweighs the avoided body transfer for large responses; - # for small ones it is a net slowdown, so it is gated by the response size here - if length >= NULL_CONNECTION_SKIP_READ_MIN_LENGTH: - kb.nullConnection = NULLCONNECTION.SKIP_READ + if kb.nullConnection is None: + _, headers, _ = Request.getPage(skipRead=True) - infoMsg = "NULL connection is supported with 'skip-read' method" - logger.info(infoMsg) - else: - debugMsg = "'skip-read' NULL connection method is available but skipped because the " - debugMsg += "response (%d B) is too small for it to outweigh the per-request reconnect cost" % length - logger.debug(debugMsg) + if HTTP_HEADER.CONTENT_LENGTH in (headers or {}): + try: + length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0]) + except ValueError: + length = len(kb.originalPage or "") + + if not _plausibleLength(length): + debugMsg = "'skip-read' method reports an implausible 'Content-Length' (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or "")) + logger.debug(debugMsg) + # Unlike HEAD/Range, 'skip-read' leaves the body unread and must close the + # connection (an unread body cannot be reused), paying a fresh TCP/TLS handshake + # per request. That only outweighs the avoided body transfer for large responses; + # for small ones it is a net slowdown, so it is gated by the response size here + elif length >= NULL_CONNECTION_SKIP_READ_MIN_LENGTH: + kb.nullConnection = NULLCONNECTION.SKIP_READ + + infoMsg = "NULL connection is supported with 'skip-read' method" + logger.info(infoMsg) + else: + debugMsg = "'skip-read' NULL connection method is available but skipped because the " + debugMsg += "response (%d B) is too small for it to outweigh the per-request reconnect cost" % length + logger.debug(debugMsg) except SqlmapConnectionException: pass diff --git a/lib/core/settings.py b/lib/core/settings.py index f750592d7..17120f469 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.6.200" +VERSION = "1.10.6.201" 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) @@ -194,6 +194,15 @@ STRUCTURAL_ID_REGEX = r"""(?si)\bid\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))"" # is a net slowdown, so it is gated by this size NULL_CONNECTION_SKIP_READ_MIN_LENGTH = 256 * 1024 +# Coarse plausibility band for a NULL connection method's reported length, relative to the known +# original page length (len(kb.originalPage)). A method is accepted only if its length falls within +# it; this rejects a method whose length does not track the real GET response (e.g. HEAD returning +# 'Content-Length: 0', HEAD served from a different code path, or sneaked-in compression). The band +# is deliberately generous (byte-vs-character size and moderate page dynamism are expected, and a +# false reject merely forgoes the optimization, which is safe) - it only catches gross mismatches +NULL_CONNECTION_LENGTH_TOLERANCE_LOW = 0.5 +NULL_CONNECTION_LENGTH_TOLERANCE_HIGH = 4.0 + # Regular expression used for recognition of IP addresses IP_ADDRESS_REGEX = r"\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b"