sqlmap/tests/test_generic_enum_more.py
Miroslav Štampar cb20a446ae
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
Update of unit tests
2026-06-28 14:28:42 +02:00

865 lines
33 KiB
Python

#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Additional unit tests for the generic enumeration mixins, deliberately targeting
branches NOT already exercised by tests/test_databases_enum.py,
tests/test_users_enum.py, tests/test_search_enum.py and tests/test_generic_more.py
(which cover the conf.direct INBAND happy paths).
This file drives the OTHER branches:
* plugins/generic/databases.py - the INFERENCE paths (conf.direct=False +
isInferenceAvailable via kb.injection BOOLEAN state: count -> per-row getValue),
the MSSQL inband-paging fallback in getDbs(), getColumns onlyColNames / dumpMode,
the getColumns MySQL<5 / ACCESS bruteforce fallback, getCount over cachedTables,
and getStatements/getProcedures empty/none branches.
* plugins/generic/users.py - getPrivileges role/grant parsing per DBMS in BOTH the
inband path (PGSQL digit columns, MySQL<5 Y/N, Firebird letters, DB2 grant codes)
and the INFERENCE path (count then per-index privilege), getPasswordHashes
grouping/dedup in the inference path, getUsers inference, isDba MSSQL.
* plugins/generic/entries.py - dumpTable INFERENCE path (count -> column-pivot via
per-(index,column) getValue), the empty-table branch, the count-failure skip,
and the resolveKeysetCursor disabling via conf.noKeyset.
* plugins/generic/search.py - searchDb / searchTable / searchColumn INFERENCE
paths (count then per-index getValue), and the MySQL<5 bruteforce branch of
searchTable / searchColumn.
Recipe (proven in tests/test_databases_enum.py): patch the module's inject.getValue
with canned rows in the EXACT shape the branch parses; for inference branches return
a positive int for EXPECTED.INT count calls then the per-row/per-index values; set the
needed kb.data flags; assert the exact resulting structure (sorted lists,
{db:{tbl:{col:type}}} dicts, privilege sets, dumpedTable values).
CRITICAL STATE HYGIENE: every test snapshots and restores conf.*, the patched
inject.getValue (per module), kb.data.cached*, kb.hintValue, kb.injection.data,
Backend/forcedDbms in tearDown so nothing leaks into the rest of the suite.
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
bootstrap()
from lib.core.data import conf, kb
from lib.core.enums import EXPECTED, PAYLOAD
import plugins.generic.databases as dbmod
import plugins.generic.users as umod
import plugins.generic.search as smod
import plugins.generic.entries as emod
from plugins.generic.databases import Databases
from plugins.generic.users import Users
from plugins.generic.search import Search
from plugins.generic.entries import Entries
_NOOP = lambda self: None
def _inference_gv(count, sequence):
"""Build an inject.getValue stub for blind inference branches.
Returns `count` (as str) whenever the caller asks for EXPECTED.INT, otherwise
yields the next item from `sequence` wrapped as a single-cell row ([value]),
cycling if exhausted. This mirrors the count-then-per-row contract of every
isInferenceAvailable() branch.
"""
state = {"i": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(count)
val = sequence[state["i"] % len(sequence)]
state["i"] += 1
return [val]
return gv
# --------------------------------------------------------------------------- #
# databases.py
# --------------------------------------------------------------------------- #
class _DbBase(unittest.TestCase):
_CONF_KEYS = ("direct", "technique", "db", "tbl", "col", "exclude",
"getComments", "excludeSysDbs", "search", "freshQueries")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_getValue = dbmod.inject.getValue
self._saved_checkBool = dbmod.inject.checkBooleanExpression
self._saved_injection_data = kb.injection.data
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_hintValue = kb.get("hintValue")
self._saved_choices = dict(kb.choices)
self._saved_readInput = dbmod.readInput
self._saved_forceDbmsEnum = getattr(Databases, "forceDbmsEnum", None)
Databases.forceDbmsEnum = _NOOP
conf.getComments = False
conf.excludeSysDbs = False
conf.exclude = None
conf.search = False
conf.freshQueries = False
conf.col = None
kb.data.has_information_schema = True
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
dbmod.inject.getValue = self._saved_getValue
dbmod.inject.checkBooleanExpression = self._saved_checkBool
dbmod.readInput = self._saved_readInput
kb.injection.data = self._saved_injection_data
kb.data.has_information_schema = self._saved_has_is
kb.hintValue = self._saved_hintValue
kb.choices.clear()
kb.choices.update(self._saved_choices)
if self._saved_forceDbmsEnum is not None:
Databases.forceDbmsEnum = self._saved_forceDbmsEnum
else:
try:
del Databases.forceDbmsEnum
except AttributeError:
pass
def _fresh(self):
d = Databases()
kb.data.currentDb = ""
kb.data.cachedDbs = []
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.data.cachedCounts = {}
kb.data.cachedStatements = []
kb.data.cachedProcedures = []
return d
def _inference(self):
conf.direct = False
conf.technique = None
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
class TestDatabasesInference(_DbBase):
def test_get_columns_inference_pgsql_types(self):
# Blind column enumeration on PostgreSQL: a count, then for each index a
# column name followed by its type. Assert the {db:{tbl:{col:type}}} parse.
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
conf.db = "public"
conf.tbl = "users"
names = ["id", "email"]
state = {"i": 0, "name": True}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return str(len(names))
if state["name"]:
val = names[state["i"] % len(names)]
state["i"] += 1
state["name"] = False
return [val]
state["name"] = True
return ["integer"]
dbmod.inject.getValue = gv
result = d.getColumns()
cols = result["public"]["users"]
self.assertEqual(len(cols), 2)
self.assertEqual(cols.get("id"), "integer")
def test_get_columns_inference_dump_mode_collist(self):
# dumpMode with an explicit conf.col list: in the inference branch the
# columns are taken straight from colList (no count/type queries at all)
# and stored with value None. Asserting no getValue ran proves the
# dump-mode shortcut, not a network round-trip.
set_dbms("MySQL")
self._inference()
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
conf.col = "id,name"
def boom(*a, **k):
raise AssertionError("dumpMode+colList must not query in inference branch")
dbmod.inject.getValue = boom
result = d.getColumns(dumpMode=True)
cols = result["testdb"]["users"]
# "name" is a reserved word -> safeSQLIdentificatorNaming backtick-quotes it;
# both columns must be present (count, since exact key varies by quoting).
self.assertEqual(len(cols), 2)
self.assertIn("id", cols)
self.assertIsNone(cols.get("id"))
def test_get_count_over_cached_tables_inference(self):
# getCount with no conf.tbl: it calls getTables() then per-table _tableGetCount.
# Drive the inband table fetch + per-table count and assert the
# {db:{count:[tables]}} grouping (tables sharing a count are grouped).
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = None
kb.data.cachedTables = {"testdb": ["users", "posts"]}
counts = {"users": "5", "posts": "5"}
def gv(query, *a, **k):
for t, c in counts.items():
if t in query:
return c
return "0"
dbmod.inject.getValue = gv
result = d.getCount()
# both tables have count 5 -> grouped under the same key
self.assertEqual(sorted(result["testdb"][5]), ["posts", "users"])
def test_get_statements_count_zero_returns_empty(self):
# Inference path: a zero count short-circuits to the (empty) cache.
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
# getStatements compares the count with the int literal 0 (count == 0), so
# the count stub must return an int 0 (not "0") to take the empty branch.
dbmod.inject.getValue = lambda query, *a, **k: 0 if k.get("expected") == EXPECTED.INT else self.fail("must not fetch rows when count is 0")
result = d.getStatements()
self.assertEqual(result, [])
def test_get_procedures_inference(self):
set_dbms("PostgreSQL")
self._inference()
d = self._fresh()
dbmod.inject.getValue = _inference_gv(2, ["sp_a", "sp_b"])
result = d.getProcedures()
self.assertEqual(sorted(result), ["sp_a", "sp_b"])
def test_get_dbs_mssql_inband_paging(self):
# MSSQL with no rows from the primary query falls into the query2 paging
# loop (one indexed query per db until a blank value stops it).
set_dbms("Microsoft SQL Server")
conf.direct = True
d = self._fresh()
dbs = ["master", "model"]
def gv(query, *a, **k):
# The primary inband query is 'SELECT name FROM master..sysdatabases'
# (no DB_NAME); make it return nothing so getDbs falls into the
# 'SELECT DB_NAME(<index>)' paging loop (query2).
if "DB_NAME" not in query:
return None
import re as _re
idx = int(_re.findall(r"DB_NAME\((\d+)\)", query)[0])
return dbs[idx] if idx < len(dbs) else ""
dbmod.inject.getValue = gv
result = d.getDbs()
self.assertEqual(sorted(result), ["master", "model"])
def test_get_tables_inference_grouped_per_db(self):
# Blind table enumeration: count for the db, then one table name per index.
set_dbms("MySQL")
self._inference()
d = self._fresh()
conf.db = "shop"
conf.tbl = None
dbmod.inject.getValue = _inference_gv(2, ["orders", "items"])
result = d.getTables()
self.assertIn("shop", result)
self.assertEqual(sorted(result["shop"]), ["items", "orders"])
class TestDatabasesBruteForce(_DbBase):
def test_get_columns_mysql_lt5_bruteforce_decline(self):
# MySQL < 5 (no information_schema) forces bruteForce in getColumns; with
# the common-column-existence prompt answered 'N' it returns None without
# issuing any column query.
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
kb.data.has_information_schema = False
kb.choices.columnExists = None
dbmod.readInput = lambda *a, **k: "N"
def boom(*a, **k):
raise AssertionError("bruteForce decline must not query columns")
dbmod.inject.getValue = boom
result = d.getColumns()
self.assertIsNone(result)
def test_get_columns_bruteforce_dumpmode_collist_on_decline(self):
# bruteForce + decline + dumpMode + colList: the columns from colList are
# stored with None type (the dump-mode salvage branch), not dropped.
set_dbms("MySQL")
conf.direct = True
d = self._fresh()
conf.db = "testdb"
conf.tbl = "users"
conf.col = "a,b"
kb.data.has_information_schema = False
kb.choices.columnExists = None
dbmod.readInput = lambda *a, **k: "N"
dbmod.inject.getValue = lambda *a, **k: None
result = d.getColumns(dumpMode=True)
cols = result["testdb"]["users"]
self.assertEqual(sorted(cols.keys()), ["a", "b"])
self.assertIsNone(cols.get("a"))
# --------------------------------------------------------------------------- #
# users.py
# --------------------------------------------------------------------------- #
class _UsersBase(unittest.TestCase):
def setUp(self):
self._direct = conf.direct
self._technique = conf.technique
self._user = conf.user
self._gv = umod.inject.getValue
self._cbe = umod.inject.checkBooleanExpression
self._store = umod.storeHashesToFile
self._attack = umod.attackCachedUsersPasswords
self._readInput = umod.readInput
self._his = kb.data.get("has_information_schema")
self._injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = True
conf.user = None
kb.data.has_information_schema = True
umod.storeHashesToFile = lambda *a, **k: None
umod.attackCachedUsersPasswords = lambda *a, **k: None
umod.readInput = lambda *a, **k: "N"
def tearDown(self):
conf.direct = self._direct
conf.technique = self._technique
conf.user = self._user
umod.inject.getValue = self._gv
umod.inject.checkBooleanExpression = self._cbe
umod.storeHashesToFile = self._store
umod.attackCachedUsersPasswords = self._attack
umod.readInput = self._readInput
kb.injection.data = self._injection_data
if self._his is None:
kb.data.pop("has_information_schema", None)
else:
kb.data.has_information_schema = self._his
def _inference(self):
conf.direct = False
conf.technique = None
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
class TestUsersPrivilegesInband(_UsersBase):
def test_privileges_pgsql_multiple_digit_columns(self):
# PostgreSQL: privilege columns are digit flags; a column index maps to
# PGSQL_PRIVS only when its value is "1". Set createdb(1)=1 and super(2)=1,
# leave the rest 0; assert exactly those two privileges are parsed and that
# "super" makes the user an admin.
set_dbms("PostgreSQL")
from lib.core.dicts import PGSQL_PRIVS
ncols = max(PGSQL_PRIVS.keys())
row = ["pguser"] + ["0"] * ncols
row[1] = "1" # createdb
row[2] = "1" # super
umod.inject.getValue = lambda query, *a, **k: [row]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertEqual(set(privileges["pguser"]), {PGSQL_PRIVS[1], PGSQL_PRIVS[2]})
self.assertIn("pguser", areAdmins)
def test_privileges_mysql_lt5_yn_flags(self):
# MySQL < 5 (no information_schema): privilege columns are 'Y'/'N' flags
# mapped to MYSQL_PRIVS by column position. Y in col 1 -> select_priv.
set_dbms("MySQL")
from lib.core.dicts import MYSQL_PRIVS
kb.data.has_information_schema = False
ncols = max(MYSQL_PRIVS.keys())
row = ["root"] + ["N"] * ncols
row[1] = "Y" # select_priv
row[3] = "Y" # update_priv
umod.inject.getValue = lambda query, *a, **k: [row]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn(MYSQL_PRIVS[1], privileges["root"])
self.assertIn(MYSQL_PRIVS[3], privileges["root"])
self.assertNotIn(MYSQL_PRIVS[2], privileges["root"])
def test_privileges_firebird_letter_codes(self):
# Firebird: each privilege is a single letter mapped via FIREBIRD_PRIVS.
set_dbms("Firebird")
from lib.core.dicts import FIREBIRD_PRIVS
umod.inject.getValue = lambda query, *a, **k: [["fbuser", "S"], ["fbuser", "I"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertEqual(set(privileges["fbuser"]),
{FIREBIRD_PRIVS["S"], FIREBIRD_PRIVS["I"]})
def test_privileges_db2_grant_codes(self):
# DB2: privilege string is "<name>,<grant-letters>"; each 'Y'/'G' letter at
# position i appends the DB2_PRIVS[i] name to the privilege.
set_dbms("DB2")
from lib.core.dicts import DB2_PRIVS
conf.user = "db2admin"
# "DBADM" plus a grant string whose first letter (position 1) is 'Y' ->
# DB2_PRIVS[1] ("CONTROLAUTH") is appended.
umod.inject.getValue = lambda query, *a, **k: [["DB2ADMIN", "DBADM,Y"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
joined = " ".join(privileges["DB2ADMIN"])
self.assertIn("DBADM", joined)
self.assertIn(DB2_PRIVS[1], joined)
class TestUsersPrivilegesInference(_UsersBase):
def test_privileges_inference_mysql(self):
# Blind privilege enumeration for a named user: count, then one privilege
# string per index. MySQL >= 5 adds each verbatim.
set_dbms("MySQL")
self._inference()
conf.user = "root"
privs = ["SELECT", "SUPER"]
umod.inject.getValue = _inference_gv(2, privs)
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
# the user key is wildcard-wrapped for the MySQL information_schema LIKE
key = [k for k in privileges if "root" in k][0]
self.assertEqual(set(privileges[key]), {"SELECT", "SUPER"})
self.assertTrue(areAdmins) # SUPER => admin
def test_privileges_inference_oracle(self):
set_dbms("Oracle")
self._inference()
conf.user = "system"
umod.inject.getValue = _inference_gv(1, ["DBA"])
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("SYSTEM", privileges)
self.assertEqual(privileges["SYSTEM"], ["DBA"])
self.assertIn("SYSTEM", areAdmins)
class TestUsersPasswordHashesInference(_UsersBase):
def test_password_hashes_inference_grouping(self):
# Blind password-hash enumeration for two users: per-user count, then one
# hash per index. Assert each user maps to its own hash list.
set_dbms("MySQL")
self._inference()
conf.user = "root,guest"
# per-user single hash; count is 1 for every user
hashes = {"root": "*ROOTHASH", "guest": "*GUESTHASH"}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return "1"
for u, h in hashes.items():
if u in query:
return [h]
return [None]
umod.inject.getValue = gv
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertEqual(res["root"], ["*ROOTHASH"])
self.assertEqual(res["guest"], ["*GUESTHASH"])
def test_password_hashes_inference_dedup(self):
# The same hash returned twice for a user must be de-duplicated at the end
# (kb.data.cachedUsersPasswords[user] = list(set(...))).
set_dbms("MySQL")
self._inference()
conf.user = "root"
umod.inject.getValue = _inference_gv(2, ["*DUP", "*DUP"])
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertEqual(res["root"], ["*DUP"])
class TestUsersGetUsersInference(_UsersBase):
def test_get_users_inference(self):
set_dbms("MySQL")
self._inference()
umod.inject.getValue = _inference_gv(2, ["root@localhost", "guest@%"])
users = Users()
kb.data.cachedUsers = []
res = users.getUsers()
self.assertEqual(sorted(res), ["guest@%", "root@localhost"])
def test_is_dba_mssql(self):
# MSSQL isDba goes through the generic checkBooleanExpression branch.
set_dbms("Microsoft SQL Server")
umod.inject.checkBooleanExpression = lambda query, *a, **k: True
users = Users()
kb.data.isDba = None
self.assertTrue(users.isDba())
# --------------------------------------------------------------------------- #
# entries.py - inference (blind) dump path
# --------------------------------------------------------------------------- #
class _RecordingDumper(object):
def __init__(self):
self.tableValues = []
def dbTableValues(self, tableValues):
self.tableValues.append(tableValues)
class _TestEntries(Entries):
def __init__(self):
Entries.__init__(self)
self.getColumnsResult = {}
self.getTablesResult = {}
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
kb.data.cachedColumns = dict(self.getColumnsResult)
def getTables(self, bruteForce=None):
kb.data.cachedTables = dict(self.getTablesResult)
class _EntriesBase(unittest.TestCase):
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "exclude", "search",
"disableHashing", "noKeyset", "keyset", "forcePivoting", "dumpWhere")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_dumper = conf.get("dumper")
self._gv = emod.inject.getValue
self._cbe = emod.inject.checkBooleanExpression
self._readInput = emod.readInput
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_cachedColumns = kb.data.get("cachedColumns")
self._saved_cachedTables = kb.data.get("cachedTables")
self._saved_dumpedTable = kb.data.get("dumpedTable")
self._saved_dumpKbInt = kb.get("dumpKeyboardInterrupt")
self._saved_permissionFlag = kb.get("permissionFlag")
self._saved_injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = False
conf.technique = None
conf.exclude = None
conf.search = False
conf.disableHashing = True
conf.noKeyset = True
conf.keyset = False
conf.forcePivoting = False
conf.dumpWhere = None
conf.dumper = _RecordingDumper()
kb.data.has_information_schema = True
kb.data.cachedColumns = {}
kb.data.cachedTables = {}
kb.data.dumpedTable = {}
kb.dumpKeyboardInterrupt = False
kb.permissionFlag = False
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
emod.readInput = lambda *a, **k: (k.get("default") if k.get("default") is not None else (a[1] if len(a) > 1 else None))
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
emod.inject.getValue = self._gv
emod.inject.checkBooleanExpression = self._cbe
emod.readInput = self._readInput
kb.data.has_information_schema = self._saved_has_is
kb.data.cachedColumns = self._saved_cachedColumns
kb.data.cachedTables = self._saved_cachedTables
kb.data.dumpedTable = self._saved_dumpedTable
kb.dumpKeyboardInterrupt = self._saved_dumpKbInt
kb.permissionFlag = self._saved_permissionFlag
kb.injection.data = self._saved_injection_data
class TestEntriesInference(_EntriesBase):
def _entries(self, db="testdb", tbl="users", cols=("id", "name")):
e = _TestEntries()
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
return e
def test_dump_table_inference_column_pivot(self):
# Blind dump (conf.direct=False, BOOLEAN available): a row count, then one
# value per (index, column). Assert the per-column pivoted values match.
set_dbms("MySQL")
e = self._entries(cols=("id", "name"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
# data[index][column] -> value. 2 rows, columns id/name.
data = {0: {"id": "1", "name": "alice"}, 1: {"id": "2", "name": "bob"}}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return "2" # row count
# MySQL blind cell query: 'SELECT <col> FROM testdb.users ORDER BY ...
# LIMIT <index>,1'. The row index is the LIMIT offset; the column is the
# SELECT projection.
import re as _re
idx = int(_re.search(r"LIMIT\s+(\d+)\s*,\s*1", query).group(1))
proj = query.split(" FROM ", 1)[0]
col = "name" if "name" in proj else "id"
return data[idx][col]
emod.inject.getValue = gv
e.dumpTable()
dumped = conf.dumper.tableValues[-1]
self.assertEqual(dumped["__infos__"]["count"], 2)
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
def test_dump_table_inference_empty_table(self):
# A zero row count in the inference path yields empty per-column value
# lists and no dbTableValues emission (dumpedTable stays effectively empty).
set_dbms("MySQL")
e = self._entries(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda query, *a, **k: ("0" if k.get("expected") == EXPECTED.INT else self.fail("must not fetch cells for empty table"))
e.dumpTable()
# count 0 => empty entries => nothing dumped
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_inference_count_failure_skips(self):
# A non-numeric count in the inference path => the table is skipped with a
# warning, no values dumped.
set_dbms("MySQL")
e = self._entries(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
return None # count failed
self.fail("must not fetch cells when count failed")
emod.inject.getValue = gv
e.dumpTable()
self.assertEqual(conf.dumper.tableValues, [])
# --------------------------------------------------------------------------- #
# search.py - inference (blind) paths
# --------------------------------------------------------------------------- #
class _TestSearch(Search):
excludeDbsList = ["information_schema", "mysql"]
def __init__(self):
Search.__init__(self)
self.like = ('2', "='%s'") # exact match (colConsider '2')
self.dumpFoundTablesCalls = []
self.dumpFoundColumnCalls = []
def likeOrExact(self, what):
return self.like
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def dumpFoundTables(self, tables):
self.dumpFoundTablesCalls.append(tables)
def dumpFoundColumn(self, dbs, foundCols, colConsider):
self.dumpFoundColumnCalls.append((dbs, foundCols, colConsider))
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
db, tbl, col = conf.db, conf.tbl, conf.col
if db and tbl:
kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {})
kb.data.cachedColumns[db][tbl][col] = "varchar"
class _RecDumper(object):
def __init__(self):
self.listed = []
self.dbTablesArg = None
self.dbColumnsArg = None
def lister(self, header, elements, content_type=None, sort=True):
self.listed.append((header, list(elements) if elements else []))
def dbTables(self, dbTables):
self.dbTablesArg = dbTables
def dbColumns(self, dbColumnsDict, colConsider, dbs):
self.dbColumnsArg = (dbColumnsDict, colConsider, dbs)
class _SearchBase(unittest.TestCase):
_CONF_KEYS = ("db", "tbl", "col", "direct", "technique", "excludeSysDbs",
"exclude", "search")
def setUp(self):
self._saved_conf = {k: conf.get(k) for k in self._CONF_KEYS}
self._saved_dumper = conf.get("dumper")
self._gv = smod.inject.getValue
self._readInput = smod.readInput
self._saved_has_is = kb.data.get("has_information_schema")
self._saved_cachedColumns = kb.data.get("cachedColumns")
self._saved_hintValue = kb.get("hintValue")
self._saved_injection_data = kb.injection.data
set_dbms("MySQL")
conf.direct = False
conf.technique = None
conf.excludeSysDbs = False
conf.exclude = None
conf.search = True
conf.dumper = _RecDumper()
kb.data.has_information_schema = True
kb.data.cachedColumns = {}
kb.injection.data = {PAYLOAD.TECHNIQUE.BOOLEAN: {"title": "AND boolean-based blind"}}
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
smod.inject.getValue = self._gv
smod.readInput = self._readInput
kb.data.has_information_schema = self._saved_has_is
kb.data.cachedColumns = self._saved_cachedColumns
kb.hintValue = self._saved_hintValue
kb.injection.data = self._saved_injection_data
class TestSearchInference(_SearchBase):
def test_search_db_inference(self):
# Blind searchDb: count of matching dbs, then one db name per index.
s = _TestSearch()
conf.db = "testdb"
smod.inject.getValue = _inference_gv(2, ["testdb", "testdb2"])
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][0], "found databases")
self.assertEqual(sorted(conf.dumper.listed[-1][1]), ["testdb", "testdb2"])
def test_search_db_inference_no_match(self):
# Count fails (non-numeric) => no databases appended, empty listing.
s = _TestSearch()
conf.db = "ghost"
smod.inject.getValue = lambda query, *a, **k: (None if k.get("expected") == EXPECTED.INT else self.fail("must not page when count fails"))
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][1], [])
def test_search_table_inference_grouped(self):
# Blind searchTable, no conf.db: outer count of dbs holding the table, then
# per-db a name, then per-db a count of matching tables, then table names.
s = _TestSearch()
conf.tbl = "users"
conf.db = None
# Sequencing by the EXPECTED.INT counts + the per-index string results.
# 1st count: number of databases with the table -> 1
# 1st db name -> "testdb"
# 2nd count: number of tables in testdb -> 1
# table name -> "users"
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
v = seq["counts"][seq["ci"] % len(seq["counts"])]
seq["ci"] += 1
return v
v = seq["vals"][seq["vi"] % len(seq["vals"])]
seq["vi"] += 1
return [v]
smod.inject.getValue = gv
s.searchTable()
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]})
self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"]})
def test_search_table_mysql_lt5_bruteforce_decline(self):
# MySQL < 5 forces the bruteforce path; declining the prompt returns None
# without any injection.
s = _TestSearch()
conf.tbl = "users"
conf.db = None
kb.data.has_information_schema = False
smod.readInput = lambda *a, **k: "N"
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
self.assertIsNone(s.searchTable())
def test_search_column_inference(self):
# Blind searchColumn, no db/tbl: count of dbs with the column, then db name;
# then per-db count of tables with the column, then table name -> getColumns
# folds the column into dbs.
s = _TestSearch()
conf.col = "password"
conf.db = None
conf.tbl = None
seq = {"counts": ["1", "1"], "ci": 0, "vals": ["testdb", "users"], "vi": 0}
def gv(query, *a, **k):
if k.get("expected") == EXPECTED.INT:
v = seq["counts"][seq["ci"] % len(seq["counts"])]
seq["ci"] += 1
return v
v = seq["vals"][seq["vi"] % len(seq["vals"])]
seq["vi"] += 1
return [v]
smod.inject.getValue = gv
s.searchColumn()
dbs = conf.dumper.dbColumnsArg[2]
self.assertIn("testdb", dbs)
self.assertIn("users", dbs["testdb"])
self.assertIn("password", dbs["testdb"]["users"])
def test_search_column_mysql_lt5_bruteforce_decline(self):
s = _TestSearch()
conf.col = "password"
conf.db = None
conf.tbl = None
kb.data.has_information_schema = False
smod.readInput = lambda *a, **k: "N"
smod.inject.getValue = lambda *a, **k: self.fail("bruteforce decline must not query")
# Declining returns None and never reaches dbColumns.
self.assertIsNone(s.searchColumn())
self.assertIsNone(conf.dumper.dbColumnsArg)
if __name__ == "__main__":
unittest.main()