From 40ccd801dce5c38700c17fb40e83cd8f2bfc0cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sat, 11 Jul 2026 09:09:08 +0200 Subject: [PATCH] Fixing desync error from SLOW_ORDER_COUNT_THRESHOLD --- data/xml/queries.xml | 2 +- lib/core/settings.py | 5 +--- lib/techniques/error/use.py | 15 ++++-------- lib/utils/keysetdump.py | 12 +++++----- plugins/generic/entries.py | 48 ++++++++++++++++++++++++++++++++++--- 5 files changed, 58 insertions(+), 24 deletions(-) diff --git a/data/xml/queries.xml b/data/xml/queries.xml index f53cf0e10..36a2c627c 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -222,7 +222,7 @@ - + diff --git a/lib/core/settings.py b/lib/core/settings.py index 332d349e6..53eb16b54 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.74" +VERSION = "1.10.7.75" 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) @@ -879,9 +879,6 @@ HASHDB_MILESTONE_VALUE = "MvKpZrBqTn" # python -c 'import random, string; print # Warn user of possible delay due to large page dump in full UNION query injections LARGE_OUTPUT_THRESHOLD = 1024 ** 2 -# On huge tables there is a considerable slowdown if every row retrieval requires ORDER BY (most noticable in table dumping using ERROR injections) -SLOW_ORDER_COUNT_THRESHOLD = 10000 - # Give up on hash recognition if nothing was found in first given number of rows HASH_RECOGNITION_QUIT_THRESHOLD = 1000 diff --git a/lib/techniques/error/use.py b/lib/techniques/error/use.py index 4b5a645c5..17511a0f4 100644 --- a/lib/techniques/error/use.py +++ b/lib/techniques/error/use.py @@ -29,7 +29,6 @@ from lib.core.common import initTechnique from lib.core.common import isListLike from lib.core.common import isNumPosStrValue from lib.core.common import listToStrValue -from lib.core.common import readInput from lib.core.common import unArrayizeValue from lib.core.common import wasLastResponseHTTPError from lib.core.compat import xrange @@ -51,7 +50,6 @@ from lib.core.settings import MIN_ERROR_CHUNK_LENGTH from lib.core.settings import NULL from lib.core.settings import PARTIAL_VALUE_MARKER from lib.core.settings import ROTATING_CHARS -from lib.core.settings import SLOW_ORDER_COUNT_THRESHOLD from lib.core.settings import SQL_SCALAR_REGEX from lib.core.settings import TURN_OFF_RESUME_INFO_LIMIT from lib.core.threads import getCurrentThreadData @@ -322,7 +320,7 @@ def errorUse(expression, dump=False): # entry at a time # NOTE: we assume that only queries that get data from a table can # return multiple entries - if (dump and (conf.limitStart or conf.limitStop)) or (" FROM " in expression.upper() and ((Backend.getIdentifiedDbms() not in FROM_DUMMY_TABLE) or (Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and not expression.upper().endswith(FROM_DUMMY_TABLE[Backend.getIdentifiedDbms()]))) and ("(CASE" not in expression.upper() or ("(CASE" in expression.upper() and "WHEN use" in expression))) and not re.search(SQL_SCALAR_REGEX, expression, re.I): + if not re.search(SQL_SCALAR_REGEX, expression, re.I) and ((dump and (conf.limitStart or conf.limitStop)) or (" FROM " in expression.upper() and ((Backend.getIdentifiedDbms() not in FROM_DUMMY_TABLE) or (Backend.getIdentifiedDbms() in FROM_DUMMY_TABLE and not expression.upper().endswith(FROM_DUMMY_TABLE[Backend.getIdentifiedDbms()]))) and ("(CASE" not in expression.upper() or ("(CASE" in expression.upper() and "WHEN use" in expression)))): expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump) if limitCond: @@ -367,13 +365,10 @@ def errorUse(expression, dump=False): return value if isNumPosStrValue(count) and int(count) > 1: - if " ORDER BY " in expression and (stopLimit - startLimit) > SLOW_ORDER_COUNT_THRESHOLD: - message = "due to huge table size do you want to remove " - message += "ORDER BY clause gaining speed over consistency? [y/N] " - - if readInput(message, default='N', boolean=True): - expression = expression[:expression.index(" ORDER BY ")] - + # NOTE: the ORDER BY clause is deliberately NOT stripped here. This path fetches each column + # of a row in a separate LIMIT/OFFSET query, so dropping ORDER BY would let the per-column + # offsets resolve to different physical rows and silently misalign cells across the row. Huge + # tables are handled cheaply and safely by keyset (seek) pagination (see plugins/generic/entries.py). numThreads = min(conf.threads, (stopLimit - startLimit)) threadData = getCurrentThreadData() diff --git a/lib/utils/keysetdump.py b/lib/utils/keysetdump.py index 2b38f2c57..eaed7b2f9 100644 --- a/lib/utils/keysetdump.py +++ b/lib/utils/keysetdump.py @@ -189,12 +189,12 @@ def _dumpSingle(tbl, colList, count, cursor, tableRef, entries, lengths): produced = 0 while produced < target: - if pivotValue is None: - query = blind.keyset_first % (field, tableRef) - else: - query = _embed(blind.keyset_next, pivotValue, field, tableRef, field) - - query = agent.whereQuery(query) + # Advance with ORDER BY ... LIMIT 1 (like the composite path), NOT MIN(): the value-extraction + # casts the aggregated column to VARCHAR *inside* MIN(), yielding a LEXICAL minimum ('10' after + # '1') that disagrees with the numeric '>' comparison and silently skips rows (2..9, 11..). The + # ORDER BY is on the raw (numeric) column, so the next cursor value is the true successor. + condition = "1=1" if pivotValue is None else "%s>%s" % (field, _lit(pivotValue)) + query = agent.whereQuery(blind.keyset_ordered % (field, tableRef, condition, field)) value = unArrayizeValue(inject.getValue(query)) if isNoneValue(value) or value == NULL: diff --git a/plugins/generic/entries.py b/plugins/generic/entries.py index 4e8ef5a7d..30ee71027 100644 --- a/plugins/generic/entries.py +++ b/plugins/generic/entries.py @@ -184,7 +184,49 @@ class Entries(object): entriesCount = 0 - if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: + def _dumpCountQuery(): + if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.SNOWFLAKE): + _ = rootQuery.blind.count % (tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper()))) + elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.MAXDB, DBMS.ACCESS, DBMS.FIREBIRD, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.RAIMA): + _ = rootQuery.blind.count % tbl + elif Backend.getIdentifiedDbms() in (DBMS.SYBASE, DBMS.MSSQL): + _ = rootQuery.blind.count % ("%s.%s" % (conf.db, tbl)) if conf.db else tbl + elif Backend.isDbms(DBMS.INFORMIX): + _ = rootQuery.blind.count % (conf.db, tbl) + else: + _ = rootQuery.blind.count % (conf.db, tbl) + return agent.whereQuery(_) + + # Keyset (seek) pagination for the error/query (inband) path too, not just the blind path below. + # It fetches every cell value-anchored (MAX(col) WHERE key=K_r on a unique integer key), so the + # per-cell retrievals of one row are pinned to the SAME physical row regardless of scan order, + # threads or MVCC - unlike ORDER BY ... LIMIT/OFFSET, which silently misaligns cells once a + # stable order is unavailable. UNION is intrinsically aligned (whole-row), so it is never preempted. + keysetDone = False + if not conf.direct and not conf.noKeyset and not isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION) \ + and any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)): + count = inject.getValue(_dumpCountQuery(), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) + if isNumPosStrValue(count) and (conf.keyset or int(count) >= KEYSET_MIN_ROWS): + keysetCursor = resolveKeysetCursor(tbl, colList) + if keysetCursor: + infoMsg = "using keyset (seek) pagination on column(s) '%s' " % ', '.join(keysetCursor) + infoMsg += "for table '%s'" % unsafeSQLIdentificatorNaming(tbl) + logger.info(infoMsg) + + try: + entries, lengths = keysetDumpTable(tbl, colList, int(count), keysetCursor) + for column, columnEntries in entries.items(): + length = max(lengths[column], getConsoleLength(column)) + kb.data.dumpedTable[column] = {"length": length, "values": columnEntries} + entriesCount = len(columnEntries) + keysetDone = bool(kb.data.dumpedTable) + except KeyboardInterrupt: + kb.dumpKeyboardInterrupt = True + clearConsoleLine() + warnMsg = "Ctrl+C detected in dumping phase" + logger.warning(warnMsg) + + if not keysetDone and (any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct): entries = [] query = None @@ -213,7 +255,7 @@ class Entries(object): for index in indexRange: row = [] for column in colList: - query = rootQuery.blind.query3 % (column, column, table, index) + query = rootQuery.blind.query3 % (column, column, prioritySortColumns(colList)[0], table, index) query = agent.whereQuery(query) value = inject.getValue(query, blind=False, time=False, dump=True) or "" row.append(value) @@ -369,7 +411,7 @@ class Entries(object): for index in indexRange: for column in colList: - query = rootQuery.blind.query3 % (column, column, table, index) + query = rootQuery.blind.query3 % (column, column, prioritySortColumns(colList)[0], table, index) query = agent.whereQuery(query) value = inject.getValue(query, union=False, error=False, dump=True) or ""