sqlmap/tests/test_users_enum.py
Miroslav Štampar 2297c81309 Update of tests
2026-06-28 18:27:59 +02:00

478 lines
19 KiB
Python

#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
Unit tests for the enumeration methods of plugins/generic/users.py.
The injection layer (lib.request.inject.getValue) is mocked so the methods can
be exercised against canned result rows without a live target, network, or DBMS.
Each test sets conf.direct = True to drive the inband (union/error/query OR
conf.direct) branch of the method under test, patches inject.getValue with rows
matching the shape the method parses, then asserts the relevant kb.data.cached*
container was populated. Inference (blind) branches set conf.direct = False with a
BOOLEAN technique present and follow the count-then-per-index contract.
"""
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.users as umod
from plugins.generic.users import Users
from lib.core.settings import CURRENT_USER
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
class TestUsersEnum(unittest.TestCase):
def setUp(self):
# Snapshot the global state these tests mutate so tearDown can restore it
# exactly (other test files share conf / kb / the inject module).
self._direct = conf.direct
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")
set_dbms("MySQL")
conf.direct = True
conf.user = None
kb.data.has_information_schema = True
# Neutralize the side effects getPasswordHashes triggers once it has
# populated the cache (file write + interactive dictionary attack prompt).
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.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
if self._his is None:
kb.data.pop("has_information_schema", None)
else:
kb.data.has_information_schema = self._his
# --- getUsers -----------------------------------------------------------
def test_get_users_mysql(self):
umod.inject.getValue = lambda query, *a, **k: [["root"], ["guest"]]
kb.data.cachedUsers = []
res = Users().getUsers()
self.assertIn("root", res)
self.assertIn("guest", res)
self.assertIn("root", kb.data.cachedUsers)
def test_get_users_postgresql(self):
set_dbms("PostgreSQL")
umod.inject.getValue = lambda query, *a, **k: [["postgres"], ["app"]]
kb.data.cachedUsers = []
res = Users().getUsers()
self.assertEqual(sorted(res), ["app", "postgres"])
def test_get_users_mssql(self):
set_dbms("Microsoft SQL Server")
umod.inject.getValue = lambda query, *a, **k: [["sa"], ["dbo"]]
kb.data.cachedUsers = []
res = Users().getUsers()
self.assertIn("sa", res)
def test_get_users_oracle(self):
set_dbms("Oracle")
umod.inject.getValue = lambda query, *a, **k: [["SYS"], ["SYSTEM"]]
kb.data.cachedUsers = []
res = Users().getUsers()
self.assertIn("SYS", res)
def test_get_users_none_leaves_cache_empty(self):
# isNoneValue([]) -> cache stays empty; inband branch skips appends.
# Strengthen: prove getUsers actually QUERIED (no stale-cache short-circuit
# returning the constant []) by spying on getValue, then in the same test
# re-run with a non-empty result to prove the cache repopulates from the
# newly fetched rows.
calls = {"n": 0}
def gv_empty(query, *a, **k):
calls["n"] += 1
return []
umod.inject.getValue = gv_empty
users = Users()
kb.data.cachedUsers = []
res = users.getUsers()
self.assertEqual(res, [])
# The inband branch must have issued at least one query, not short-circuited.
self.assertGreaterEqual(calls["n"], 1)
# Paired non-empty case: same instance, fresh cache, real rows -> cache
# must repopulate with exactly those users.
umod.inject.getValue = lambda query, *a, **k: [["root"], ["guest"]]
kb.data.cachedUsers = []
res2 = users.getUsers()
self.assertEqual(sorted(res2), ["guest", "root"])
self.assertIn("root", kb.data.cachedUsers)
# --- getCurrentUser -----------------------------------------------------
def test_get_current_user(self):
umod.inject.getValue = lambda query, *a, **k: "root@localhost"
users = Users()
kb.data.currentUser = ""
self.assertEqual(users.getCurrentUser(), "root@localhost")
self.assertEqual(kb.data.currentUser, "root@localhost")
# --- isDba --------------------------------------------------------------
def test_is_dba_mysql(self):
umod.inject.getValue = lambda query, *a, **k: "root@localhost"
umod.inject.checkBooleanExpression = lambda query, *a, **k: True
users = Users()
kb.data.currentUser = ""
kb.data.isDba = None
self.assertTrue(users.isDba())
def test_is_dba_postgresql_false(self):
set_dbms("PostgreSQL")
umod.inject.checkBooleanExpression = lambda query, *a, **k: False
users = Users()
kb.data.isDba = None
self.assertFalse(users.isDba())
# --- getPasswordHashes --------------------------------------------------
def test_get_password_hashes_mysql(self):
# filterPairValues keeps length-2 rows -> {user: [hash]}
umod.inject.getValue = lambda query, *a, **k: [["root", "*ABC123"], ["guest", "*DEF456"]]
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertIn("root", res)
self.assertIn("guest", res)
self.assertEqual(res["root"], ["*ABC123"])
def test_get_password_hashes_with_conf_user(self):
conf.user = "root@localhost"
umod.inject.getValue = lambda query, *a, **k: [["root", "*HASH"]]
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertIn("root", res)
def test_get_password_hashes_oracle(self):
set_dbms("Oracle")
conf.user = "system"
umod.inject.getValue = lambda query, *a, **k: [["SYSTEM", "ABCDEF1234567890"]]
users = Users()
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertIn("SYSTEM", res)
# conf.user upper-cased for Oracle
self.assertEqual(conf.user, "SYSTEM")
def test_get_password_hashes_current_user(self):
conf.user = CURRENT_USER
# First getValue resolves current user, subsequent ones return the rows.
def gv(query, *a, **k):
if "CURRENT_USER" in query.upper() or "current_user" in query:
return "root@localhost"
return [["root", "*HASH"]]
umod.inject.getValue = gv
users = Users()
kb.data.currentUser = ""
kb.data.cachedUsersPasswords = {}
res = users.getPasswordHashes()
self.assertIn("root", res)
# --- getPrivileges ------------------------------------------------------
def test_get_privileges_mysql(self):
# MySQL with information_schema: privilege column added verbatim.
umod.inject.getValue = lambda query, *a, **k: [["root", "SUPER"], ["guest", "SELECT"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("root", privileges)
self.assertIn("SUPER", privileges["root"])
self.assertIn("root", areAdmins)
self.assertNotIn("guest", areAdmins)
def test_get_privileges_postgresql(self):
set_dbms("PostgreSQL")
from lib.core.dicts import PGSQL_PRIVS
# PGSQL: digit columns map to PGSQL_PRIVS by column index; col 1 == True.
idx = sorted(PGSQL_PRIVS.keys())[0]
row = ["pguser"] + ["0"] * (max(PGSQL_PRIVS.keys()))
row[idx] = "1"
umod.inject.getValue = lambda query, *a, **k: [row]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("pguser", privileges)
self.assertIn(PGSQL_PRIVS[idx], privileges["pguser"])
def test_get_privileges_oracle(self):
set_dbms("Oracle")
umod.inject.getValue = lambda query, *a, **k: [["SYS", "DBA"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("SYS", privileges)
self.assertIn("DBA", privileges["SYS"])
self.assertIn("SYS", areAdmins)
def test_get_privileges_with_conf_user(self):
conf.user = "root"
umod.inject.getValue = lambda query, *a, **k: [["root", "SELECT"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getPrivileges()
self.assertIn("root", privileges)
# --- getRoles (delegates to getPrivileges) ------------------------------
def test_get_roles(self):
umod.inject.getValue = lambda query, *a, **k: [["root", "SUPER"]]
users = Users()
kb.data.cachedUsersPrivileges = {}
privileges, areAdmins = users.getRoles()
self.assertIn("root", privileges)
self.assertIn("root", areAdmins)
# --------------------------------------------------------------------------- #
# Privilege parsing / inference branches (relocated from test_generic_enum_more.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())
if __name__ == "__main__":
unittest.main()