Adding keyset (seek) pagination for faster blind table dumps
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-06-20 00:00:40 +02:00
parent 889ad43541
commit 497d3772bd
7 changed files with 355 additions and 15 deletions

View file

@ -189,7 +189,7 @@ ccc4a717e887652b1fcce073d9409d9c59a3b28548c703a9e453d15845f90cd7 lib/core/patch
48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
fcb89f3b6474c6201fe2a77417c5c422e4f81a5f44567a51fb05eb6f6df22e93 lib/core/settings.py
8411f42e10133c779cff837c6e51698cfebe0796f93ca9e3575a5644d64a3e04 lib/core/settings.py
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
@ -253,6 +253,7 @@ a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps
0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py
71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py
1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py
04b28ad98340a589eb9b21d014c435e8193c2bea3a21af9875b6f23c9b270f1f lib/utils/pivotdumptable.py
c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py
c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py

View file

@ -61,7 +61,8 @@
</columns>
<dump_table>
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s.%s"/>
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s.%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 LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
<primary_key count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s' AND CONSTRAINT_NAME='PRIMARY'" query="SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s' AND CONSTRAINT_NAME='PRIMARY' ORDER BY ORDINAL_POSITION LIMIT %d,1"/>
</dump_table>
<search_db>
<inband query="SELECT schema_name FROM INFORMATION_SCHEMA.SCHEMATA WHERE %s" query2="SELECT db FROM mysql.db WHERE %s" condition="schema_name" condition2="db"/>
@ -136,7 +137,8 @@
</columns>
<dump_table>
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
<blind query="SELECT %s FROM %s.%s ORDER BY %s OFFSET %d LIMIT 1" count="SELECT COUNT(*) FROM %s.%s"/>
<blind query="SELECT %s FROM %s.%s ORDER BY %s OFFSET %d LIMIT 1" count="SELECT COUNT(*) FROM %s.%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 LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
<primary_key count="SELECT COUNT(*) FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name=kcu.constraint_name AND tc.table_schema=kcu.table_schema WHERE tc.constraint_type='PRIMARY KEY' AND tc.table_schema='%s' AND tc.table_name='%s'" query="SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name=kcu.constraint_name AND tc.table_schema=kcu.table_schema WHERE tc.constraint_type='PRIMARY KEY' AND tc.table_schema='%s' AND tc.table_name='%s' ORDER BY kcu.ordinal_position OFFSET %d LIMIT 1"/>
</dump_table>
<search_db>
<inband query="SELECT schemaname FROM pg_tables WHERE %s" condition="schemaname"/>
@ -207,7 +209,8 @@
</columns>
<dump_table>
<inband query="SELECT %s FROM %s.%s"/>
<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"/>
<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"/>
<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"/>
</dump_table>
<search_db>
<inband query="SELECT name FROM master..sysdatabases WHERE %s" condition="name"/>
@ -302,7 +305,8 @@
</columns>
<dump_table>
<inband query="SELECT %s FROM %s ORDER BY ROWNUM"/>
<blind query="SELECT %s FROM (SELECT qq.*,ROWNUM AS CAP FROM %s qq ORDER BY ROWNUM) WHERE CAP=%d" count="SELECT COUNT(*) FROM %s"/>
<blind query="SELECT %s FROM (SELECT qq.*,ROWNUM AS CAP FROM %s qq ORDER BY ROWNUM) WHERE CAP=%d" count="SELECT COUNT(*) 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 c FROM (SELECT %s AS c FROM %s WHERE %s ORDER BY %s) WHERE ROWNUM=1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
<primary_key count="SELECT COUNT(*) FROM all_cons_columns cols, all_constraints cons WHERE cons.constraint_type='P' AND cons.constraint_name=cols.constraint_name AND cons.owner=cols.owner AND UPPER(cols.owner)=UPPER('%s') AND UPPER(cols.table_name)=UPPER('%s')" query="SELECT column_name FROM (SELECT cols.column_name, ROW_NUMBER() OVER (ORDER BY cols.position) AS rn FROM all_cons_columns cols, all_constraints cons WHERE cons.constraint_type='P' AND cons.constraint_name=cols.constraint_name AND cons.owner=cols.owner AND UPPER(cols.owner)=UPPER('%s') AND UPPER(cols.table_name)=UPPER('%s')) WHERE rn=%d+1"/>
</dump_table>
<!-- NOTE: in Oracle schema names are the counterpart to database names on other DBMSes -->
<search_db>
@ -362,7 +366,7 @@
</columns>
<dump_table>
<inband query="SELECT %s FROM %s"/>
<blind query="SELECT %s FROM %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s"/>
<blind query="SELECT %s FROM %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s" rowid="rowid" 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 LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
</dump_table>
<search_db/>
<search_table>
@ -726,7 +730,8 @@
<inband query="SELECT column_name,type_name FROM INFORMATION_SCHEMA.SYSTEM_COLUMNS WHERE table_name='%s' AND table_schem='%s' ORDER BY column_name" condition="column_name"/>
</columns>
<dump_table>
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s"/>
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%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 LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
<primary_key count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA=kcu.TABLE_SCHEMA WHERE tc.CONSTRAINT_TYPE='PRIMARY KEY' AND UPPER(tc.TABLE_SCHEMA)=UPPER('%s') AND UPPER(tc.TABLE_NAME)=UPPER('%s')" query="SELECT kcu.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA=kcu.TABLE_SCHEMA WHERE tc.CONSTRAINT_TYPE='PRIMARY KEY' AND UPPER(tc.TABLE_SCHEMA)=UPPER('%s') AND UPPER(tc.TABLE_NAME)=UPPER('%s') ORDER BY kcu.ORDINAL_POSITION LIMIT 1 OFFSET %d"/>
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
</dump_table>
<search_db>
@ -790,7 +795,8 @@
<inband query="SELECT COLUMN_NAME,DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='%s' AND TABLE_SCHEMA='%s' ORDER BY COLUMN_NAME" condition="COLUMN_NAME" query2="SELECT COLUMN_NAME,TYPE_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='%s' AND TABLE_SCHEMA='%s' ORDER BY COLUMN_NAME" condition2="COLUMN_NAME"/>
</columns>
<dump_table>
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s"/>
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%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 LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
<primary_key count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA=kcu.TABLE_SCHEMA WHERE tc.CONSTRAINT_TYPE='PRIMARY KEY' AND UPPER(tc.TABLE_SCHEMA)=UPPER('%s') AND UPPER(tc.TABLE_NAME)=UPPER('%s')" query="SELECT kcu.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA=kcu.TABLE_SCHEMA WHERE tc.CONSTRAINT_TYPE='PRIMARY KEY' AND UPPER(tc.TABLE_SCHEMA)=UPPER('%s') AND UPPER(tc.TABLE_NAME)=UPPER('%s') ORDER BY kcu.ORDINAL_POSITION LIMIT 1 OFFSET %d"/>
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
</dump_table>
<search_db>
@ -1445,7 +1451,8 @@
</columns>
<dump_table>
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s"/>
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%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 LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
<primary_key count="SELECT COUNT(*) FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name=kcu.constraint_name AND tc.table_schema=kcu.table_schema WHERE tc.constraint_type='PRIMARY KEY' AND tc.table_schema='%s' AND tc.table_name='%s'" query="SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name=kcu.constraint_name AND tc.table_schema=kcu.table_schema WHERE tc.constraint_type='PRIMARY KEY' AND tc.table_schema='%s' AND tc.table_name='%s' ORDER BY kcu.ordinal_position LIMIT 1 OFFSET %d"/>
</dump_table>
<search_db>
<inband query="SELECT schema_name FROM information_schema.schemata WHERE %s" condition="schema_name"/>

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.6.129"
VERSION = "1.10.6.130"
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

@ -84,6 +84,7 @@ def vulnTest():
("-u <url> --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")),
("-u <url> --technique=U --fresh-queries --force-partial --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("performed 31 queries", "nameisnull", "~using default dictionary", "dumped to HTML file")),
("-u <url> --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")),
("-u <url> --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor
("-u <url> -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")),
("-u \"<url>&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)),
("-u \"<url>&query=*\" --flush-session --technique=Q --banner", ("Title: SQLite inline queries", "banner: '3.")),

312
lib/utils/keysetdump.py Normal file
View file

@ -0,0 +1,312 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import re
from lib.core.agent import agent
from lib.core.bigarray import BigArray
from lib.core.common import Backend
from lib.core.common import isNoneValue
from lib.core.common import singleTimeWarnMessage
from lib.core.common import unArrayizeValue
from lib.core.common import unsafeSQLIdentificatorNaming
from lib.core.compat import xrange
from lib.core.convert import getConsoleLength
from lib.core.convert import getUnicode
from lib.core.data import conf
from lib.core.data import logger
from lib.core.data import queries
from lib.core.dicts import DUMP_REPLACEMENTS
from lib.core.enums import CHARSET_TYPE
from lib.core.enums import DBMS
from lib.core.enums import EXPECTED
from lib.core.settings import NULL
from lib.core.unescaper import unescaper
from lib.request import inject
from lib.utils.safe2bin import safechardecode
# back-end DBMSes whose dump table reference is schema/database-qualified (db.table).
# Note: for MSSQL the table identifier already carries its schema (e.g. dbo.users), so the
# plain db.table form yields the correct db.schema.table (e.g. [master].dbo.users).
KEYSET_SCHEMA_QUALIFIED = (DBMS.MYSQL, DBMS.PGSQL, DBMS.CRATEDB, DBMS.MSSQL, DBMS.H2, DBMS.HSQLDB)
def _tableRef(tbl):
dbms = Backend.getIdentifiedDbms()
if dbms in (DBMS.ORACLE,) and conf.db:
return "%s.%s" % (conf.db.upper(), tbl.upper())
if dbms in KEYSET_SCHEMA_QUALIFIED and conf.db:
return "%s.%s" % (conf.db, tbl)
return tbl
def keysetSupported():
"""
Whether the back-end DBMS declares the keyset (seek) pagination queries and a
cursor source (a physical row-id pseudo-column or a primary-key catalog lookup)
"""
dumpNode = queries[Backend.getIdentifiedDbms()].dump_table
return "keyset_next" in dumpNode.blind and ("rowid" in dumpNode.blind or "primary_key" in dumpNode)
def _integerCursor(tbl, cursor):
"""
Whether every cursor column holds integer values, probed via MIN(col).
Only integer keys are accepted: _embed() emits them as bare numeric literals, giving a
numeric comparison that matches MIN/ORDER BY. String (and even decimal) keys would be
escaped to a binary/hex literal whose order can differ from MIN's collation and silently
skip rows, so they are rejected here and fall back to the OFFSET dump.
"""
blind = queries[Backend.getIdentifiedDbms()].dump_table.blind
ref = _tableRef(tbl)
for column in cursor:
query = agent.whereQuery(blind.keyset_first % (agent.preprocessField(tbl, column), ref))
value = unArrayizeValue(inject.getValue(query))
# empty/NULL MIN (e.g. empty table) is not disqualifying; the walk just yields no rows
if not isNoneValue(value) and re.match(r"\A-?[0-9]+\Z", getUnicode(value).strip()) is None:
return False
return True
def resolveKeysetCursor(tbl, colList):
"""
Returns the list of column(s) forming a stable, indexed cursor for keyset (seek)
pagination of the table: a declared physical row-id pseudo-column when available,
otherwise the indexed primary key (single or composite) resolved from the catalog.
Returns None when neither applies or a key column is not part of the dumped columns.
"""
if not keysetSupported():
return None
dumpNode = queries[Backend.getIdentifiedDbms()].dump_table
# 1) a declared physical row-id pseudo-column (always unique + indexed where supported)
if "rowid" in dumpNode.blind:
return [dumpNode.blind.rowid]
# 2) the indexed primary key (single-column, or composite when keyset_ordered is declared)
pkNode = dumpNode.primary_key
# Note: schema/table are string literals in the catalog lookups, so the unquoted
# (identifier-unescaped) names are used (the dump queries keep the quoted form)
unsafeDb = unsafeSQLIdentificatorNaming(conf.db)
unsafeTbl = unsafeSQLIdentificatorNaming(tbl)
# Note: no whereQuery() here - these are catalog (schema) lookups, so the data-row
# filter from --where must not be appended to them
query = pkNode.count % (unsafeDb, unsafeTbl)
count = inject.getValue(query, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
try:
count = int(count)
except (ValueError, TypeError):
return None
if count < 1:
return None
# composite keys require the row-value/ordered keyset form
if count > 1 and "keyset_ordered" not in dumpNode.blind:
return None
cursor = []
for index in xrange(count):
query = pkNode.query % (unsafeDb, unsafeTbl, index)
column = unArrayizeValue(inject.getValue(query))
if not column:
return None
match = None
for _ in colList:
if _ and _.lower() == column.lower():
match = _
break
if match is None:
return None
cursor.append(match)
# restrict to integer cursors: a string key's escaped-literal comparison may order
# differently than MIN/ORDER BY and silently skip rows (such keys fall back to OFFSET)
if not _integerCursor(tbl, cursor):
return None
return cursor
def _lit(value):
"""
Type-correct SQL literal for a cursor value: a bare numeric literal for numeric keys
(so the index is still used and the comparison is numeric), otherwise the DBMS-escaped
(e.g. 0x.. hex) form for string keys. Both forms are self-contained (no surrounding quotes).
"""
if value is not None and re.match(r"\A-?[0-9]+\Z", value):
return value
return unescaper.escape(value, False)
def _embed(template, value, *fixed):
"""
Fills a single-column keyset template whose trailing placeholder is the cursor value.
"""
template = template.replace("'%s'", "%s")
return template % (fixed + (_lit(value),))
def _dumpSingle(tbl, colList, count, cursor, tableRef, entries, lengths):
blind = queries[Backend.getIdentifiedDbms()].dump_table.blind
field = agent.preprocessField(tbl, cursor)
if conf.limitStart and conf.limitStop:
target = max(0, conf.limitStop - conf.limitStart + 1)
elif conf.limitStop:
target = conf.limitStop
elif conf.limitStart:
target = max(0, count - conf.limitStart + 1)
else:
target = count
pivotValue = None
# hybrid: a single OFFSET jump to seed the cursor just before --start, then pure keyset
if conf.limitStart and conf.limitStart > 1 and "keyset_seed" in blind:
query = agent.whereQuery(blind.keyset_seed % (field, tableRef, field, conf.limitStart - 2))
seed = unArrayizeValue(inject.getValue(query))
if isNoneValue(seed) or seed == NULL:
return
pivotValue = safechardecode(seed)
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)
value = unArrayizeValue(inject.getValue(query))
if isNoneValue(value) or value == NULL:
break
value = safechardecode(value)
# safety latch against a non-advancing cursor (e.g. encoding edge cases)
if value == pivotValue:
singleTimeWarnMessage("keyset cursor stopped advancing prematurely")
break
pivotValue = value
for column in colList:
if column == cursor:
colValue = pivotValue
else:
query = _embed(blind.keyset_by, pivotValue, agent.preprocessField(tbl, column), tableRef, field)
query = agent.whereQuery(query)
colValue = unArrayizeValue(inject.getValue(query, dump=True))
colValue = "" if isNoneValue(colValue) else colValue
lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(colValue), getUnicode(colValue))))
entries[column].append(colValue)
produced += 1
def _dumpComposite(tbl, colList, count, cursorCols, tableRef, entries, lengths):
blind = queries[Backend.getIdentifiedDbms()].dump_table.blind
fields = [agent.preprocessField(tbl, _) for _ in cursorCols]
orderExpr = ','.join(fields)
startSkip = (conf.limitStart - 1) if conf.limitStart else 0
if conf.limitStart and conf.limitStop:
target = max(0, conf.limitStop - conf.limitStart + 1)
elif conf.limitStop:
target = conf.limitStop
elif conf.limitStart:
target = max(0, count - conf.limitStart + 1)
else:
target = count
prev = None
produced = 0
seen = 0
while produced < target and seen < count:
if prev is None:
condition = "1=1"
else:
# ANSI row-value (tuple) comparison advances the composite cursor lexicographically
condition = "(%s)>(%s)" % (orderExpr, ','.join(_lit(_) for _ in prev))
tup = []
for field in fields:
query = agent.whereQuery(blind.keyset_ordered % (field, tableRef, condition, orderExpr))
value = unArrayizeValue(inject.getValue(query))
tup.append(None if isNoneValue(value) else safechardecode(value))
if all(isNoneValue(_) for _ in tup):
break
if prev is not None and tup == prev:
singleTimeWarnMessage("keyset cursor stopped advancing prematurely")
break
prev = tup
seen += 1
if seen <= startSkip:
continue
equals = " AND ".join("%s=%s" % (field, _lit(value)) for field, value in zip(fields, tup))
for column in colList:
if column in cursorCols:
colValue = tup[cursorCols.index(column)]
else:
query = agent.whereQuery(blind.keyset_where % (agent.preprocessField(tbl, column), tableRef, equals))
colValue = unArrayizeValue(inject.getValue(query, dump=True))
colValue = "" if isNoneValue(colValue) else colValue
lengths[column] = max(lengths[column], getConsoleLength(DUMP_REPLACEMENTS.get(getUnicode(colValue), getUnicode(colValue))))
entries[column].append(colValue)
produced += 1
def keysetDumpTable(tbl, colList, count, cursor):
"""
Dumps a table one row at a time using keyset (seek) pagination on 'cursor' (a list of
one or more indexed key columns): the next row is reached with a >/row-value comparison
against the previous cursor (index range scan) and every other column is fetched with an
exact equality on the cursor (index point seek), so no row is skipped via OFFSET and no
per-row ORDER BY filesort is needed. A deep --start uses a single OFFSET "seed" jump
(single-column cursors), after which the walk is pure keyset.
"""
tableRef = _tableRef(tbl)
lengths = {}
entries = {}
for column in colList:
lengths[column] = 0
entries[column] = BigArray()
if len(cursor) == 1:
_dumpSingle(tbl, colList, count, cursor[0], tableRef, entries, lengths)
else:
_dumpComposite(tbl, colList, count, cursor, tableRef, entries, lengths)
debugMsg = "keyset pagination retrieved %d row(s) for table '%s'" % (len(entries[colList[0]]) if colList and colList[0] in entries else 0, unsafeSQLIdentificatorNaming(tbl))
logger.debug(debugMsg)
return entries, lengths

View file

@ -14,6 +14,7 @@ from lib.core.common import filterNone
from lib.core.common import getSafeExString
from lib.core.common import isNoneValue
from lib.core.common import isNumPosStrValue
from lib.core.common import prioritySortColumns
from lib.core.common import singleTimeWarnMessage
from lib.core.common import unArrayizeValue
from lib.core.common import unsafeSQLIdentificatorNaming
@ -29,7 +30,6 @@ from lib.core.enums import CHARSET_TYPE
from lib.core.enums import EXPECTED
from lib.core.exception import SqlmapConnectionException
from lib.core.exception import SqlmapNoneDataException
from lib.core.settings import MAX_INT
from lib.core.settings import NULL
from lib.core.settings import SINGLE_QUOTE_MARKER
from lib.core.unescaper import unescaper
@ -71,7 +71,7 @@ def pivotDumpTable(table, colList, count=None, blind=True, alias=None):
lengths[column] = 0
entries[column] = BigArray()
colList = filterNone(sorted(colList, key=lambda x: len(x) if x else MAX_INT))
colList = prioritySortColumns(filterNone(colList))
if conf.pivotColumn:
for _ in colList:

View file

@ -42,12 +42,15 @@ from lib.core.exception import SqlmapNoneDataException
from lib.core.exception import SqlmapUnsupportedFeatureException
from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD
from lib.core.settings import CURRENT_DB
from lib.core.settings import KEYSET_MIN_ROWS
from lib.core.settings import METADB_SUFFIX
from lib.core.settings import NULL
from lib.core.settings import PLUS_ONE_DBMSES
from lib.core.settings import UPPER_CASE_DBMSES
from lib.request import inject
from lib.utils.hash import attackDumpedTable
from lib.utils.keysetdump import keysetDumpTable
from lib.utils.keysetdump import resolveKeysetCursor
from lib.utils.pivotdumptable import pivotDumpTable
from thirdparty import six
from thirdparty.six.moves import zip as _zip
@ -309,6 +312,9 @@ class Entries(object):
count = inject.getValue(query, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
# keyset (seek) pagination: forced with --keyset, automatic for large tables, off with --no-keyset
keysetCursor = resolveKeysetCursor(tbl, colList) if (not conf.noKeyset and isNumPosStrValue(count) and (conf.keyset or int(count) >= KEYSET_MIN_ROWS)) else None
lengths = {}
entries = {}
@ -332,6 +338,19 @@ class Entries(object):
continue
elif 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, count, keysetCursor)
except KeyboardInterrupt:
kb.dumpKeyboardInterrupt = True
clearConsoleLine()
warnMsg = "Ctrl+C detected in dumping phase"
logger.warning(warnMsg)
elif Backend.getIdentifiedDbms() in (DBMS.ACCESS, DBMS.SYBASE, DBMS.MAXDB, DBMS.MSSQL, DBMS.INFORMIX, DBMS.MCKOI, DBMS.RAIMA):
if Backend.getIdentifiedDbms() in (DBMS.ACCESS, DBMS.MCKOI, DBMS.RAIMA):
table = tbl
@ -411,17 +430,17 @@ class Entries(object):
entries[column] = BigArray()
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE, DBMS.SPANNER):
query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, sorted(colList, key=len)[0], index)
query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, prioritySortColumns(colList)[0], index)
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE,):
query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), index)
elif Backend.getIdentifiedDbms() in (DBMS.MIMERSQL,):
query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), sorted(colList, key=len)[0], index)
query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), prioritySortColumns(colList)[0], index)
elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.EXTREMEDB):
query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl, index)
elif Backend.isDbms(DBMS.FIREBIRD):
query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), tbl)
elif Backend.getIdentifiedDbms() in (DBMS.INFORMIX, DBMS.VIRTUOSO):
query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl, sorted(colList, key=len)[0])
query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl, prioritySortColumns(colList)[0])
elif Backend.isDbms(DBMS.FRONTBASE):
query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), conf.db, tbl)
else: