Update of tests

This commit is contained in:
Miroslav Štampar 2026-06-28 18:27:59 +02:00
parent cb20a446ae
commit 2297c81309
32 changed files with 7177 additions and 7304 deletions

View file

@ -6,11 +6,19 @@ See the file 'LICENSE' for copying permission
DBMS-specific enumeration overrides (plugins/dbms/<dbms>/enumeration.py),
driven through each full DBMS handler with the injection layer mocked, so the
dialect-specific table/column discovery paths run without a live target. The
in-band (UNION/error/direct) branch is taken via conf.direct=True and
inject.getValue is stubbed with canned result rows.
dialect-specific table/column/user/privilege discovery paths run without a live
target, network, or DBMS. The in-band (UNION/error/direct) branch is taken via
conf.direct=True and inject.getValue is stubbed with canned result rows;
conf.batch=True avoids interactive prompts.
Consolidated from former tests/test_dbms_enum.py (Microsoft SQL Server),
tests/test_dbms_enum_a.py (Oracle/PostgreSQL/MySQL/SQLite) and
tests/test_dbms_enum_b.py (Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB).
stdlib unittest only (no pytest / no pip); works on Python 2.7 and 3.x.
"""
import importlib
import os
import sys
import unittest
@ -19,11 +27,18 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms
bootstrap()
from lib.core.common import Backend
from lib.core.data import conf, kb
from lib.core.enums import EXPECTED
from lib.core.exception import SqlmapUnsupportedFeatureException
from lib.request import inject
class _EnumBase(unittest.TestCase):
# ---------------------------------------------------------------------------
# Base for Microsoft SQL Server getTables (former test_dbms_enum.py)
# ---------------------------------------------------------------------------
class _EnumBaseMSSQL(unittest.TestCase):
"""Snapshot/restore the global state these enumerators mutate."""
module = None # the enumeration module whose inject.getValue we patch
@ -45,7 +60,7 @@ class _EnumBase(unittest.TestCase):
kb.data.cachedColumns = self._cachedColumns
class TestMSSQLServerEnum(_EnumBase):
class TestMSSQLServerEnum(_EnumBaseMSSQL):
import plugins.dbms.mssqlserver.enumeration as module
def _handler(self):
@ -94,5 +109,614 @@ class TestMSSQLServerEnum(_EnumBase):
self.assertEqual(tables["salesdb"], ["dbo.invoices", "dbo.orders"])
# ---------------------------------------------------------------------------
# Base for Oracle/PostgreSQL/MySQL/SQLite (former test_dbms_enum_a.py)
# ---------------------------------------------------------------------------
class _EnumBaseA(unittest.TestCase):
"""Snapshot/restore the global state these enumerators mutate.
Other tests in the suite depend on clean globals (a leaked kb.hintValue
breaks test_inference_engine; a leaked forced DBMS breaks others), so every
knob touched here is captured in setUp and put back in tearDown.
"""
# the enumeration module whose inject.getValue we patch (overridden per DBMS)
module = None
def setUp(self):
# conf knobs
self._direct = conf.direct
self._batch = conf.batch
self._user = conf.user
self._db = conf.get("db")
self._tbl = conf.get("tbl")
self._exclude = conf.get("exclude")
# injection layer (some override modules - e.g. SQLite/PostgreSQL - do not
# import inject because their overrides return constants without querying)
self._has_inject = hasattr(self.module, "inject")
if self._has_inject:
self._gv = self.module.inject.getValue
# kb.data cached* containers
self._cachedTables = kb.data.get("cachedTables")
self._cachedColumns = kb.data.get("cachedColumns")
self._cachedDbs = kb.data.get("cachedDbs")
self._cachedUsers = kb.data.get("cachedUsers")
self._cachedUsersRoles = kb.data.get("cachedUsersRoles")
self._cachedUsersPrivileges = kb.data.get("cachedUsersPrivileges")
self._has_information_schema = kb.data.get("has_information_schema")
# state other tests are sensitive to
self._hintValue = kb.hintValue
self._injectionData = kb.injection.data
self._forcedDbms = Backend.getForcedDbms()
self._stickyDBMS = kb.stickyDBMS
# avoid readInput EOFError flakiness and interactive prompts
conf.direct = True
conf.batch = True
def tearDown(self):
conf.direct = self._direct
conf.batch = self._batch
conf.user = self._user
conf.db = self._db
conf.tbl = self._tbl
conf.exclude = self._exclude
if self._has_inject:
self.module.inject.getValue = self._gv
kb.data.cachedTables = self._cachedTables
kb.data.cachedColumns = self._cachedColumns
kb.data.cachedDbs = self._cachedDbs
kb.data.cachedUsers = self._cachedUsers
kb.data.cachedUsersRoles = self._cachedUsersRoles
kb.data.cachedUsersPrivileges = self._cachedUsersPrivileges
kb.data.has_information_schema = self._has_information_schema
kb.hintValue = self._hintValue
kb.injection.data = self._injectionData
kb.stickyDBMS = self._stickyDBMS
if self._forcedDbms is not None:
Backend.forceDbms(self._forcedDbms)
else:
kb.forcedDbms = None
class TestOracleEnum(_EnumBaseA):
module = importlib.import_module("plugins.dbms.oracle.enumeration")
def _handler(self):
from plugins.dbms.oracle import OracleMap
set_dbms("Oracle")
return OracleMap()
def test_get_roles(self):
# rows are [GRANTEE, GRANTED_ROLE]; first column is the user, the rest roles
conf.user = None
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [
["SYS", "DBA"], ["SYS", "CONNECT"], ["SCOTT", "RESOURCE"]
]
roles, areAdmins = self._handler().getRoles()
self.assertIn("SYS", roles)
self.assertIn("SCOTT", roles)
self.assertEqual(set(roles["SYS"]), {"DBA", "CONNECT"})
# DBA implies administrator
self.assertIn("SYS", areAdmins)
def test_get_roles_filtered_by_user(self):
# conf.user populates a WHERE clause; canned rows still drive the parse
conf.user = "SCOTT"
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [["SCOTT", "RESOURCE"]]
roles, _ = self._handler().getRoles()
self.assertEqual(list(roles.keys()), ["SCOTT"])
self.assertEqual(roles["SCOTT"], ["RESOURCE"])
def test_get_roles_multiple_roles_per_user(self):
# a user appearing across several rows accumulates all granted roles
conf.user = None
kb.data.cachedUsersRoles = {}
self.module.inject.getValue = lambda q, *a, **k: [
["APP", "CONNECT"], ["APP", "RESOURCE"], ["APP", "CREATE SESSION"]
]
roles, _ = self._handler().getRoles()
self.assertEqual(
set(roles["APP"]), {"CONNECT", "RESOURCE", "CREATE SESSION"}
)
class TestPostgreSQLEnum(_EnumBaseA):
module = importlib.import_module("plugins.dbms.postgresql.enumeration")
def _handler(self):
from plugins.dbms.postgresql import PostgreSQLMap
set_dbms("PostgreSQL")
return PostgreSQLMap()
def test_get_hostname_unsupported(self):
# PostgreSQL overrides getHostname purely to warn; it returns None
self.assertIsNone(self._handler().getHostname())
class TestMySQLEnum(_EnumBaseA):
# MySQL's enumeration.py adds no overrides (it is a bare `pass`); cover the
# generic discovery path through the full MySQL handler instead.
module = importlib.import_module("plugins.generic.enumeration")
def _handler(self):
from plugins.dbms.mysql import MySQLMap
set_dbms("MySQL")
return MySQLMap()
def test_get_dbs(self):
conf.db = None
kb.data.cachedDbs = []
kb.data.has_information_schema = True
self.module.inject.getValue = lambda q, *a, **k: (
3 if k.get("expected") == EXPECTED.INT
else [["information_schema"], ["testdb"], ["mysql"]]
)
dbs = self._handler().getDbs()
self.assertIn("testdb", dbs)
self.assertEqual(set(kb.data.cachedDbs), set(dbs))
class TestSQLiteEnum(_EnumBaseA):
module = importlib.import_module("plugins.dbms.sqlite.enumeration")
def _handler(self):
from plugins.dbms.sqlite import SQLiteMap
set_dbms("SQLite")
return SQLiteMap()
def test_unsupported_simple_overrides(self):
# SQLite overrides these to a warning + an empty/neutral return value
h = self._handler()
self.assertIsNone(h.getCurrentUser())
self.assertIsNone(h.getCurrentDb())
self.assertIsNone(h.getHostname())
self.assertEqual(h.getUsers(), [])
self.assertEqual(h.getDbs(), [])
self.assertEqual(h.searchDb(), [])
self.assertEqual(h.getStatements(), [])
self.assertEqual(h.getPasswordHashes(), {})
self.assertEqual(h.getPrivileges(), {})
def test_is_dba_always_true(self):
# on SQLite the current user is treated as having all privileges
self.assertTrue(self._handler().isDba())
def test_search_column_raises(self):
with self.assertRaises(SqlmapUnsupportedFeatureException):
self._handler().searchColumn()
# ---------------------------------------------------------------------------
# Base + helpers for Sybase/MaxDB/MSSQL extra/DB2/Informix/Firebird/HSQLDB
# (former test_dbms_enum_b.py)
# ---------------------------------------------------------------------------
def _fresh_cached():
kb.data.cachedDbs = []
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.data.cachedUsers = []
kb.data.cachedUsersPrivileges = {}
kb.data.cachedCounts = {}
kb.data.cachedStatements = []
kb.data.banner = None
class _NoOpDumper(object):
"""Swallow every dumper call so search methods don't emit/prompt."""
def __getattr__(self, name):
return lambda *a, **k: None
def _handler(display_name, dirname):
"""Instantiate the full *Map handler for the given DBMS."""
set_dbms(display_name)
main = importlib.import_module("plugins.dbms.%s" % dirname)
cls = [getattr(main, n) for n in dir(main) if n.endswith("Map")][0]
return cls()
class _EnumBaseB(unittest.TestCase):
"""Snapshot/restore every global these enumerators mutate."""
# subclasses set these
display_name = None
dirname = None
def setUp(self):
# config snapshot
self._direct = conf.direct
self._batch = conf.batch
self._db = conf.db
self._tbl = conf.tbl
self._col = conf.col
self._user = conf.user
self._exclude = conf.exclude
self._search = conf.search
self._getBanner = conf.getBanner
self._excludeSysDbs = conf.excludeSysDbs
self._dumper = conf.get("dumper")
# kb snapshot
self._cached = {k: kb.data.get(k) for k in (
"cachedDbs", "cachedTables", "cachedColumns", "cachedUsers",
"cachedUsersPrivileges", "cachedCounts", "cachedStatements", "banner",
)}
self._hintValue = kb.hintValue
self._injectionData = kb.injection.data
self._currentDb = kb.data.get("currentDb")
self._hasIS = kb.data.get("has_information_schema")
# injection layer snapshot
self._gv = inject.getValue
self._cbe = getattr(inject, "checkBooleanExpression", None)
# baseline config the in-band/non-interactive paths need
conf.direct = True
conf.batch = True
kb.data.has_information_schema = True
_fresh_cached()
# restore the chosen DBMS for every test
self.handler = _handler(self.display_name, self.dirname)
# the enumeration module whose pivotDumpTable some tests stub
self.em = importlib.import_module("plugins.dbms.%s.enumeration" % self.dirname)
def tearDown(self):
conf.direct = self._direct
conf.batch = self._batch
conf.db = self._db
conf.tbl = self._tbl
conf.col = self._col
conf.user = self._user
conf.exclude = self._exclude
conf.search = self._search
conf.getBanner = self._getBanner
conf.excludeSysDbs = self._excludeSysDbs
conf.dumper = self._dumper
for k, v in self._cached.items():
kb.data[k] = v
kb.hintValue = self._hintValue
kb.injection.data = self._injectionData
kb.data.currentDb = self._currentDb
kb.data.has_information_schema = self._hasIS
inject.getValue = self._gv
if self._cbe is not None:
inject.checkBooleanExpression = self._cbe
if hasattr(self.em, "pivotDumpTable"):
# restore the pristine reference from the wrapper module
import lib.utils.pivotdumptable as _pdt
self.em.pivotDumpTable = _pdt.pivotDumpTable
# ---------------------------------------------------------------------------
# Sybase
# ---------------------------------------------------------------------------
class TestSybaseEnum(_EnumBaseB):
display_name = "Sybase"
dirname = "sybase"
def _pivot(self, *value_lists):
"""Make em.pivotDumpTable return canned (entries, lengths) per call.
Each successive call pops the next mapping of {colName: [values]}.
"""
calls = list(value_lists)
def fake(table, colList, count=None, blind=True, alias=None):
mapping = calls.pop(0) if calls else {}
entries = {}
lengths = {}
for col in colList:
vals = mapping.get(col.split(".")[-1], [])
entries[col] = list(vals)
lengths[col] = 0
return entries, lengths
self.em.pivotDumpTable = fake
def test_get_users(self):
self._pivot({"name": ["sa", "guest"]})
users = self.handler.getUsers()
self.assertIn("sa", users)
self.assertIn("guest", users)
def test_get_dbs(self):
self._pivot({"name": ["master", "model"]})
dbs = self.handler.getDbs()
self.assertEqual(sorted(dbs), ["master", "model"])
def test_get_tables(self):
conf.db = "testdb"
self._pivot({"name": ["users", "logs"]})
tables = self.handler.getTables()
self.assertIn("testdb", tables)
self.assertEqual(sorted(tables["testdb"]), ["logs", "users"])
def test_get_columns(self):
conf.db = "testdb"
conf.tbl = "users"
# column pivot returns name + usertype: REAL Sybase numeric type ids that
# getColumns resolves through SYBASE_TYPES (7 -> "int", 2 -> "varchar").
from lib.core.dicts import SYBASE_TYPES
self._pivot({"name": ["id", "name"], "usertype": ["7", "2"]})
cols = self.handler.getColumns()
self.assertIn("testdb", cols)
# table key is identifier-normalized (may be schema-qualified)
tbls = cols["testdb"]
self.assertTrue(any("users" in t for t in tbls))
colset = list(tbls.values())[0]
# the VALUE is the resolved type name, not the raw usertype number:
# proves the SYBASE_TYPES numeric->name mapping actually ran.
self.assertEqual(colset["id"], SYBASE_TYPES[7]) # "int"
self.assertEqual(colset["name"], SYBASE_TYPES[2]) # "varchar"
def test_get_privileges(self):
# getPrivileges -> getUsers (pivot) then isDba (checkBooleanExpression).
# Drive the admin-set branch BOTH ways via the isDba oracle so the result
# is not forced by a constant-True stub.
conf.user = None
# oracle True: every user is flagged DBA -> admins == all users
self._pivot({"name": ["sa", "guest"]})
inject.checkBooleanExpression = lambda *a, **k: True
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs) # users still enumerated as privilege keys
self.assertIn("guest", privs)
self.assertEqual(admins, set(["sa", "guest"]))
# oracle False: nobody is a DBA -> admins is empty, but users still listed
_fresh_cached()
self._pivot({"name": ["sa", "guest"]})
inject.checkBooleanExpression = lambda *a, **k: False
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set())
def test_search_not_implemented(self):
# these intentionally return [] with a warning on Sybase
self.assertEqual(self.handler.searchDb(), [])
self.assertEqual(self.handler.searchTable(), [])
self.assertEqual(self.handler.searchColumn(), [])
def test_get_hostname(self):
# not possible on Sybase; just must not raise
self.assertIsNone(self.handler.getHostname())
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# SAP MaxDB
# ---------------------------------------------------------------------------
class TestMaxDBEnum(_EnumBaseB):
display_name = "SAP MaxDB"
dirname = "maxdb"
def _pivot(self, *value_lists):
calls = list(value_lists)
def fake(table, colList, count=None, blind=True, alias=None):
mapping = calls.pop(0) if calls else {}
entries = {}
lengths = {}
for col in colList:
vals = mapping.get(col.split(".")[-1], [])
entries[col] = list(vals)
lengths[col] = 0
return entries, lengths
self.em.pivotDumpTable = fake
def test_get_dbs(self):
self._pivot({"schemaname": ["SYSTEM", "DOMAIN"]})
dbs = self.handler.getDbs()
self.assertEqual(sorted(dbs), ["DOMAIN", "SYSTEM"])
def test_get_tables(self):
conf.db = "SYSTEM"
self._pivot({"tablename": ["USERS", "TABLES"]})
tables = self.handler.getTables()
# db key is identifier-normalized (uppercase names get quoted)
self.assertEqual(len(tables), 1)
tbls = list(tables.values())[0]
self.assertEqual(sorted(tbls), ["TABLES", "USERS"])
def test_get_columns(self):
conf.db = "SYSTEM"
conf.tbl = "USERS"
self._pivot({
"columnname": ["ID", "NAME"],
"datatype": ["INTEGER", "CHAR"],
"len": ["4", "32"],
})
cols = self.handler.getColumns()
self.assertEqual(len(cols), 1)
tbls = list(cols.values())[0]
self.assertIn("USERS", tbls)
self.assertEqual(tbls["USERS"]["ID"], "INTEGER(4)")
def test_get_privileges_empty(self):
self.assertEqual(self.handler.getPrivileges(), {})
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Microsoft SQL Server (methods NOT covered by TestMSSQLServerEnum above)
# ---------------------------------------------------------------------------
class TestMSSQLServerExtraEnum(_EnumBaseB):
display_name = "Microsoft SQL Server"
dirname = "mssqlserver"
def test_get_privileges(self):
# getPrivileges -> getUsers (generic, inject.getValue) then isDba.
# Exercise the admin-set branch BOTH ways via the isDba oracle.
conf.user = None
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
# oracle True: all users flagged DBA
inject.checkBooleanExpression = lambda *a, **k: True
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set(["sa", "BUILTIN\\Administrators"]))
# oracle False: none are DBA -> empty admin set, users still enumerated
_fresh_cached()
inject.getValue = lambda q, *a, **k: ["sa", "BUILTIN\\Administrators"]
inject.checkBooleanExpression = lambda *a, **k: False
privs, admins = self.handler.getPrivileges()
self.assertIn("sa", privs)
self.assertEqual(admins, set())
def test_search_table(self):
conf.db = "testdb"
conf.tbl = "users"
# in-band branch: getValue returns matching table name(s)
inject.getValue = lambda q, *a, **k: ["users"]
# capture the discovered tables instead of dumping them
captured = {}
conf.dumper = _NoOpDumper()
self.handler.dumpFoundTables = lambda tables: captured.update(tables)
self.handler.searchTable()
# at least one database mapped to the matched table
flat = set()
for tbls in captured.values():
flat.update(tbls)
self.assertTrue(any("users" in t for t in flat))
def test_search_column(self):
conf.db = "testdb"
conf.tbl = None
conf.col = "password"
# exact match (no wildcard) so no recursive getColumns call;
# getValue returns the tables that contain the column
inject.getValue = lambda q, *a, **k: ["users"]
captured = {}
conf.dumper = _NoOpDumper()
self.handler.dumpFoundColumn = lambda dbs, foundCols, colConsider: captured.update(dbs)
self.handler.searchColumn()
# the searched column was located in at least one table
flat = set()
for tbls in captured.values():
flat.update(tbls)
self.assertTrue(any("users" in t for t in flat))
# ---------------------------------------------------------------------------
# IBM DB2
# ---------------------------------------------------------------------------
class TestDB2Enum(_EnumBaseB):
display_name = "IBM DB2"
dirname = "db2"
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Informix
# ---------------------------------------------------------------------------
class TestInformixEnum(_EnumBaseB):
display_name = "Informix"
dirname = "informix"
def test_search_db(self):
self.assertEqual(self.handler.searchDb(), [])
def test_search_table(self):
self.assertEqual(self.handler.searchTable(), [])
def test_search_column(self):
self.assertEqual(self.handler.searchColumn(), [])
def test_get_statements(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# Firebird
# ---------------------------------------------------------------------------
class TestFirebirdEnum(_EnumBaseB):
display_name = "Firebird"
dirname = "firebird"
def test_get_dbs_empty(self):
self.assertEqual(self.handler.getDbs(), [])
def test_get_password_hashes_empty(self):
self.assertEqual(self.handler.getPasswordHashes(), {})
def test_search_db_empty(self):
self.assertEqual(self.handler.searchDb(), [])
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
# ---------------------------------------------------------------------------
# HSQLDB
# ---------------------------------------------------------------------------
class TestHSQLDBEnum(_EnumBaseB):
display_name = "HSQLDB"
dirname = "hsqldb"
def test_get_banner(self):
conf.getBanner = True
kb.data.banner = None
# getValue returns a single-element LIST; getBanner pipes it through
# unArrayizeValue, which must unwrap it to the scalar banner string.
inject.getValue = lambda q, *a, **k: ["HSQLDB 2.5.1"]
banner = self.handler.getBanner()
self.assertEqual(banner, "HSQLDB 2.5.1")
def test_get_privileges_empty(self):
self.assertEqual(self.handler.getPrivileges(), {})
def test_get_hostname(self):
self.assertIsNone(self.handler.getHostname())
def test_get_statements_empty(self):
self.assertEqual(self.handler.getStatements(), [])
def test_get_current_db_default_schema(self):
from lib.core.settings import HSQLDB_DEFAULT_SCHEMA
self.assertEqual(self.handler.getCurrentDb(), HSQLDB_DEFAULT_SCHEMA)
if __name__ == "__main__":
unittest.main()