Skip to content

Commit 40ccd80

Browse files
committed
Fixing desync error from SLOW_ORDER_COUNT_THRESHOLD
1 parent 1cdc69e commit 40ccd80

5 files changed

Lines changed: 58 additions & 24 deletions

File tree

data/xml/queries.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@
222222
</columns>
223223
<dump_table>
224224
<inband query="SELECT %s FROM %s.%s"/>
225-
<blind query="SELECT MIN(%s) FROM %s WHERE CONVERT(NVARCHAR(4000),%s)>'%s'" query2="SELECT MAX(%s) FROM %s WHERE CONVERT(NVARCHAR(4000),%s) LIKE '%s'" query3="SELECT %s FROM (SELECT %s, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS CAP FROM %s)x WHERE CAP=%d" count="SELECT LTRIM(STR(COUNT(*))) FROM %s" count2="SELECT LTRIM(STR(COUNT(DISTINCT(%s)))) FROM %s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY" keyset_ordered="SELECT TOP 1 %s FROM %s WHERE %s ORDER BY %s" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
225+
<blind query="SELECT MIN(%s) FROM %s WHERE CONVERT(NVARCHAR(4000),%s)>'%s'" query2="SELECT MAX(%s) FROM %s WHERE CONVERT(NVARCHAR(4000),%s) LIKE '%s'" query3="SELECT %s FROM (SELECT %s, ROW_NUMBER() OVER (ORDER BY %s) AS CAP FROM %s)x WHERE CAP=%d" count="SELECT LTRIM(STR(COUNT(*))) FROM %s" count2="SELECT LTRIM(STR(COUNT(DISTINCT(%s)))) FROM %s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY" keyset_ordered="SELECT TOP 1 %s FROM %s WHERE %s ORDER BY %s" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
226226
<primary_key count="SELECT COUNT(*) FROM sys.indexes i JOIN sys.index_columns ic ON i.object_id=ic.object_id AND i.index_id=ic.index_id WHERE i.is_primary_key=1 AND i.object_id=OBJECT_ID('%s.dbo.%s')" query="SELECT name FROM (SELECT c.name AS name, ROW_NUMBER() OVER (ORDER BY ic.key_ordinal) AS rn FROM sys.indexes i JOIN sys.index_columns ic ON i.object_id=ic.object_id AND i.index_id=ic.index_id JOIN sys.columns c ON ic.object_id=c.object_id AND c.column_id=ic.column_id WHERE i.is_primary_key=1 AND i.object_id=OBJECT_ID('%s.dbo.%s')) x WHERE rn=%d+1"/>
227227
</dump_table>
228228
<search_db>

lib/core/settings.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.7.74"
23+
VERSION = "1.10.7.75"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
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 @@
879879
# Warn user of possible delay due to large page dump in full UNION query injections
880880
LARGE_OUTPUT_THRESHOLD = 1024 ** 2
881881

882-
# On huge tables there is a considerable slowdown if every row retrieval requires ORDER BY (most noticable in table dumping using ERROR injections)
883-
SLOW_ORDER_COUNT_THRESHOLD = 10000
884-
885882
# Give up on hash recognition if nothing was found in first given number of rows
886883
HASH_RECOGNITION_QUIT_THRESHOLD = 1000
887884

lib/techniques/error/use.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from lib.core.common import isListLike
3030
from lib.core.common import isNumPosStrValue
3131
from lib.core.common import listToStrValue
32-
from lib.core.common import readInput
3332
from lib.core.common import unArrayizeValue
3433
from lib.core.common import wasLastResponseHTTPError
3534
from lib.core.compat import xrange
@@ -51,7 +50,6 @@
5150
from lib.core.settings import NULL
5251
from lib.core.settings import PARTIAL_VALUE_MARKER
5352
from lib.core.settings import ROTATING_CHARS
54-
from lib.core.settings import SLOW_ORDER_COUNT_THRESHOLD
5553
from lib.core.settings import SQL_SCALAR_REGEX
5654
from lib.core.settings import TURN_OFF_RESUME_INFO_LIMIT
5755
from lib.core.threads import getCurrentThreadData
@@ -322,7 +320,7 @@ def errorUse(expression, dump=False):
322320
# entry at a time
323321
# NOTE: we assume that only queries that get data from a table can
324322
# return multiple entries
325-
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):
323+
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)))):
326324
expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump)
327325

328326
if limitCond:
@@ -367,13 +365,10 @@ def errorUse(expression, dump=False):
367365
return value
368366

369367
if isNumPosStrValue(count) and int(count) > 1:
370-
if " ORDER BY " in expression and (stopLimit - startLimit) > SLOW_ORDER_COUNT_THRESHOLD:
371-
message = "due to huge table size do you want to remove "
372-
message += "ORDER BY clause gaining speed over consistency? [y/N] "
373-
374-
if readInput(message, default='N', boolean=True):
375-
expression = expression[:expression.index(" ORDER BY ")]
376-
368+
# NOTE: the ORDER BY clause is deliberately NOT stripped here. This path fetches each column
369+
# of a row in a separate LIMIT/OFFSET query, so dropping ORDER BY would let the per-column
370+
# offsets resolve to different physical rows and silently misalign cells across the row. Huge
371+
# tables are handled cheaply and safely by keyset (seek) pagination (see plugins/generic/entries.py).
377372
numThreads = min(conf.threads, (stopLimit - startLimit))
378373

379374
threadData = getCurrentThreadData()

lib/utils/keysetdump.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,12 @@ def _dumpSingle(tbl, colList, count, cursor, tableRef, entries, lengths):
189189
produced = 0
190190

191191
while produced < target:
192-
if pivotValue is None:
193-
query = blind.keyset_first % (field, tableRef)
194-
else:
195-
query = _embed(blind.keyset_next, pivotValue, field, tableRef, field)
196-
197-
query = agent.whereQuery(query)
192+
# Advance with ORDER BY ... LIMIT 1 (like the composite path), NOT MIN(): the value-extraction
193+
# casts the aggregated column to VARCHAR *inside* MIN(), yielding a LEXICAL minimum ('10' after
194+
# '1') that disagrees with the numeric '>' comparison and silently skips rows (2..9, 11..). The
195+
# ORDER BY is on the raw (numeric) column, so the next cursor value is the true successor.
196+
condition = "1=1" if pivotValue is None else "%s>%s" % (field, _lit(pivotValue))
197+
query = agent.whereQuery(blind.keyset_ordered % (field, tableRef, condition, field))
198198
value = unArrayizeValue(inject.getValue(query))
199199

200200
if isNoneValue(value) or value == NULL:

plugins/generic/entries.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,49 @@ def dumpTable(self, foundData=None):
184184

185185
entriesCount = 0
186186

187-
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
187+
def _dumpCountQuery():
188+
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.SNOWFLAKE):
189+
_ = rootQuery.blind.count % (tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())))
190+
elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.MAXDB, DBMS.ACCESS, DBMS.FIREBIRD, DBMS.MCKOI, DBMS.EXTREMEDB, DBMS.RAIMA):
191+
_ = rootQuery.blind.count % tbl
192+
elif Backend.getIdentifiedDbms() in (DBMS.SYBASE, DBMS.MSSQL):
193+
_ = rootQuery.blind.count % ("%s.%s" % (conf.db, tbl)) if conf.db else tbl
194+
elif Backend.isDbms(DBMS.INFORMIX):
195+
_ = rootQuery.blind.count % (conf.db, tbl)
196+
else:
197+
_ = rootQuery.blind.count % (conf.db, tbl)
198+
return agent.whereQuery(_)
199+
200+
# Keyset (seek) pagination for the error/query (inband) path too, not just the blind path below.
201+
# It fetches every cell value-anchored (MAX(col) WHERE key=K_r on a unique integer key), so the
202+
# per-cell retrievals of one row are pinned to the SAME physical row regardless of scan order,
203+
# threads or MVCC - unlike ORDER BY ... LIMIT/OFFSET, which silently misaligns cells once a
204+
# stable order is unavailable. UNION is intrinsically aligned (whole-row), so it is never preempted.
205+
keysetDone = False
206+
if not conf.direct and not conf.noKeyset and not isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION) \
207+
and any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)):
208+
count = inject.getValue(_dumpCountQuery(), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
209+
if isNumPosStrValue(count) and (conf.keyset or int(count) >= KEYSET_MIN_ROWS):
210+
keysetCursor = resolveKeysetCursor(tbl, colList)
211+
if keysetCursor:
212+
infoMsg = "using keyset (seek) pagination on column(s) '%s' " % ', '.join(keysetCursor)
213+
infoMsg += "for table '%s'" % unsafeSQLIdentificatorNaming(tbl)
214+
logger.info(infoMsg)
215+
216+
try:
217+
entries, lengths = keysetDumpTable(tbl, colList, int(count), keysetCursor)
218+
for column, columnEntries in entries.items():
219+
length = max(lengths[column], getConsoleLength(column))
220+
kb.data.dumpedTable[column] = {"length": length, "values": columnEntries}
221+
entriesCount = len(columnEntries)
222+
keysetDone = bool(kb.data.dumpedTable)
223+
except KeyboardInterrupt:
224+
kb.dumpKeyboardInterrupt = True
225+
clearConsoleLine()
226+
warnMsg = "Ctrl+C detected in dumping phase"
227+
logger.warning(warnMsg)
228+
229+
if not keysetDone and (any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct):
188230
entries = []
189231
query = None
190232

@@ -213,7 +255,7 @@ def dumpTable(self, foundData=None):
213255
for index in indexRange:
214256
row = []
215257
for column in colList:
216-
query = rootQuery.blind.query3 % (column, column, table, index)
258+
query = rootQuery.blind.query3 % (column, column, prioritySortColumns(colList)[0], table, index)
217259
query = agent.whereQuery(query)
218260
value = inject.getValue(query, blind=False, time=False, dump=True) or ""
219261
row.append(value)
@@ -369,7 +411,7 @@ def dumpTable(self, foundData=None):
369411

370412
for index in indexRange:
371413
for column in colList:
372-
query = rootQuery.blind.query3 % (column, column, table, index)
414+
query = rootQuery.blind.query3 % (column, column, prioritySortColumns(colList)[0], table, index)
373415
query = agent.whereQuery(query)
374416

375417
value = inject.getValue(query, union=False, error=False, dump=True) or ""

0 commit comments

Comments
 (0)