sqlmap/tests/test_search_enum.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

475 lines
18 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 plugins/generic/search.py (Search) and plugins/generic/entries.py
(Entries), exercising searchDb / searchTable / searchColumn and dumpTable by
MOCKING the injection layer (lib.request.inject.getValue) and the dumper.
No network and no DBMS are involved: conf.direct=True selects the simple inband
branches, inject.getValue is patched to return canned rows in the exact shape the
methods parse, and conf.dumper is replaced with a recording stub so we can assert
on what each method produced (kb.data caches / returned dicts).
"""
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
import plugins.generic.search as smod
import plugins.generic.entries as emod
from plugins.generic.search import Search
from plugins.generic.entries import Entries
class _RecordingDumper(object):
"""Minimal stand-in for conf.dumper that records calls instead of printing/writing."""
def __init__(self):
self.reset()
def reset(self):
self.listed = [] # (header, elements)
self.dbTablesArg = None
self.dbColumnsArg = None
self.dbTableColumnsArg = None
self.tableValues = []
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)
def dbTableColumns(self, tableColumns, content_type=None):
self.dbTableColumnsArg = tableColumns
def dbTableValues(self, tableValues):
self.tableValues.append(tableValues)
class _TestSearch(Search):
"""Search with the cross-mixin collaborators it relies on stubbed out.
The real Search lives in a multiple-inheritance hierarchy; in isolation we
must supply likeOrExact/forceDbmsEnum/getCurrentDb/getColumns/dumpFoundTables/
excludeDbsList, mirroring the inputs the production mixins would provide.
"""
excludeDbsList = ["information_schema", "mysql"]
def __init__(self):
Search.__init__(self)
self.like = ('1', " LIKE '%%%s%%'")
self.dumpFoundTablesCalls = []
self.dumpFoundColumnCalls = []
self.getColumnsCalls = []
self._cannedColumns = {}
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):
# Emulate column discovery by populating kb.data.cachedColumns for the
# currently-targeted conf.db/conf.tbl/conf.col, as the real plugin does.
self.getColumnsCalls.append((conf.db, conf.tbl, conf.col))
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 _TestEntries(Entries):
"""Entries with cross-mixin collaborators stubbed (forceDbmsEnum/getCurrentDb/getColumns/getTables)."""
def __init__(self):
Entries.__init__(self)
self.getColumnsResult = {} # {db: {tbl: {col: type}}}
self.getTablesResult = {} # value assigned to kb.data.cachedTables
self.getColumnsCalls = []
def forceDbmsEnum(self):
pass
def getCurrentDb(self):
return "testdb"
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
self.getColumnsCalls.append((conf.db, conf.tbl))
kb.data.cachedColumns = dict(self.getColumnsResult)
def getTables(self, bruteForce=None):
kb.data.cachedTables = dict(self.getTablesResult)
class _SearchEnumBase(unittest.TestCase):
def setUp(self):
# Save mutated globals
self._saved_conf = {k: conf.get(k) for k in (
"db", "tbl", "col", "direct", "excludeSysDbs", "exclude", "search",
"disableHashing", "noKeyset", "keyset", "forcePivoting",
)}
self._saved_dumper = conf.get("dumper")
self._search_getValue = smod.inject.getValue
self._entries_getValue = emod.inject.getValue
self._search_readInput = smod.readInput
self._entries_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")
set_dbms("MySQL")
conf.direct = True
conf.excludeSysDbs = False
conf.exclude = None
conf.search = True
conf.disableHashing = True
conf.noKeyset = True
conf.keyset = False
conf.forcePivoting = False
conf.dumper = _RecordingDumper()
kb.data.has_information_schema = True
kb.data.cachedColumns = {}
kb.data.cachedTables = {}
kb.data.dumpedTable = {}
kb.dumpKeyboardInterrupt = False
kb.permissionFlag = False
# Non-interactive prompts: collapse readInput to its default.
def _readInput(message, default=None, checkBatch=True, boolean=False):
if boolean:
return True if (default in (None, 'Y', 'y', True)) else False
return default
smod.readInput = _readInput
emod.readInput = _readInput
def tearDown(self):
for k, v in self._saved_conf.items():
conf[k] = v
conf.dumper = self._saved_dumper
smod.inject.getValue = self._search_getValue
emod.inject.getValue = self._entries_getValue
smod.readInput = self._search_readInput
emod.readInput = self._entries_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
class TestSearch(_SearchEnumBase):
# --- searchDb -----------------------------------------------------------
def test_search_db_found(self):
s = _TestSearch()
conf.db = "testdb"
# Feed identifiers that REQUIRE normalization: "select" is a reserved
# keyword and "weird db" contains a space; both force MySQL backtick
# quoting via safeSQLIdentificatorNaming. The plain "testdb" passes
# through unchanged. Asserting the quoted output proves the transform ran
# (a mock-echo would surface the raw, unquoted inputs instead).
smod.inject.getValue = lambda *a, **k: ["select", "weird db", "testdb"]
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][0], "found databases")
self.assertEqual(conf.dumper.listed[-1][1], ["`select`", "`weird db`", "testdb"])
def test_search_db_multiple_terms(self):
s = _TestSearch()
conf.db = "foo,bar"
# Return a DISTINCT value per search term by keying off the query string:
# each term is folded into the generated query (search_db inband query),
# so "foo" vs "bar" produce different queries. This proves the method
# actually iterates over both terms and records the right match for each,
# rather than appending the same constant twice.
def gv(query, *a, **k):
if "foo" in query:
return "foo_db"
elif "bar" in query:
return "bar_db"
return None
smod.inject.getValue = gv
s.searchDb()
# Two search terms => one distinct value found per term, in order.
self.assertEqual(conf.dumper.listed[-1][1], ["foo_db", "bar_db"])
def test_search_db_none_value(self):
s = _TestSearch()
conf.db = "nope"
smod.inject.getValue = lambda *a, **k: None
s.searchDb()
self.assertEqual(conf.dumper.listed[-1][1], [])
def test_search_db_exclude_sys_dbs(self):
s = _TestSearch()
conf.db = "testdb"
conf.excludeSysDbs = True
seen = {}
def gv(query, *a, **k):
seen["query"] = query
return ["testdb"]
smod.inject.getValue = gv
s.searchDb()
# The exclusion clause for each system DB must be folded into the query.
self.assertIn("information_schema", seen["query"])
self.assertEqual(conf.dumper.listed[-1][1], ["testdb"])
# --- searchTable --------------------------------------------------------
def test_search_table_found_grouped_by_db(self):
s = _TestSearch()
conf.tbl = "users"
conf.db = None
smod.inject.getValue = lambda *a, **k: [["testdb", "users"], ["otherdb", "users"]]
s.searchTable()
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"], "otherdb": ["users"]})
# dumpFoundTables is invoked with the same mapping.
self.assertEqual(s.dumpFoundTablesCalls[-1], {"testdb": ["users"], "otherdb": ["users"]})
def test_search_table_with_db_filter(self):
s = _TestSearch()
conf.tbl = "users"
conf.db = "testdb"
captured = {}
def gv(query, *a, **k):
captured["query"] = query
return [["testdb", "users"]]
smod.inject.getValue = gv
s.searchTable()
# conf.db present => a WHERE clause restricting to that db is appended.
self.assertIn("testdb", captured["query"])
self.assertEqual(conf.dumper.dbTablesArg, {"testdb": ["users"]})
def test_search_table_none_found(self):
s = _TestSearch()
conf.tbl = "ghost"
conf.db = None
smod.inject.getValue = lambda *a, **k: None
s.searchTable()
# No tables => dbTables/dumpFoundTables never called.
self.assertIsNone(conf.dumper.dbTablesArg)
self.assertEqual(s.dumpFoundTablesCalls, [])
# --- searchColumn -------------------------------------------------------
def test_search_column_db_and_tbl_provided(self):
s = _TestSearch()
conf.col = "password"
conf.db = "testdb"
conf.tbl = "users"
# With both db & tbl set, searchColumn does NOT call inject for table
# discovery; it assumes the provided db/tbl and calls getColumns.
smod.inject.getValue = lambda *a, **k: self.fail("getValue should not be called")
s.searchColumn()
self.assertEqual(conf.dumper.dbColumnsArg[1], '1') # colConsider
dbs = conf.dumper.dbColumnsArg[2]
self.assertIn("testdb", dbs)
self.assertIn("users", dbs["testdb"])
self.assertIn("password", dbs["testdb"]["users"])
self.assertEqual(s.dumpFoundColumnCalls[-1][2], '1')
# getColumns was consulted for the assumed db/tbl/col.
self.assertIn(("testdb", "users", "password"), s.getColumnsCalls)
def test_search_column_enumerate_tables(self):
s = _TestSearch()
conf.col = "password"
conf.db = None
conf.tbl = None
# db & tbl missing => inject returns (db, table) pairs to scan.
smod.inject.getValue = lambda *a, **k: [["testdb", "users"]]
s.searchColumn()
dbs = conf.dumper.dbColumnsArg[2]
self.assertIn("testdb", dbs)
self.assertIn("users", dbs["testdb"])
# The requested column must have actually landed under the discovered
# db/table (getColumns populated kb.data.cachedColumns, which searchColumn
# folds into dbs); db/table presence alone wouldn't prove that.
self.assertIn("password", dbs["testdb"]["users"])
def test_search_column_none_found(self):
s = _TestSearch()
conf.col = "password"
conf.db = None
conf.tbl = None
smod.inject.getValue = lambda *a, **k: None
s.searchColumn()
# Nothing discovered => dumper.dbColumns not called.
self.assertIsNone(conf.dumper.dbColumnsArg)
# --- search() dispatcher ------------------------------------------------
def test_search_dispatch_to_column(self):
s = _TestSearch()
conf.col = "password"
conf.tbl = "users"
conf.db = "testdb"
smod.inject.getValue = lambda *a, **k: None
# Should route to searchColumn (col takes precedence). With db & tbl both
# provided, searchColumn assumes them and consults getColumns for the
# requested db/tbl/col -> at least one recorded call, matching the request.
s.search()
self.assertGreaterEqual(len(s.getColumnsCalls), 1)
self.assertIn(("testdb", "users", "password"), s.getColumnsCalls)
def test_search_dispatch_missing_param(self):
s = _TestSearch()
conf.col = None
conf.tbl = None
conf.db = None
from lib.core.exception import SqlmapMissingMandatoryOptionException
self.assertRaises(SqlmapMissingMandatoryOptionException, s.search)
class TestEntries(_SearchEnumBase):
def _entries_with_cols(self, db="testdb", tbl="users", cols=("id", "name")):
e = _TestEntries()
e.getColumnsResult = {db: {tbl: {c: "varchar" for c in cols}}}
return e
# --- dumpTable: inband (conf.direct) ------------------------------------
def test_dump_table_inband_rows(self):
e = self._entries_with_cols(cols=("id", "name"))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
# MySQL inband dump returns a list of [colVal, colVal] rows.
emod.inject.getValue = lambda *a, **k: [["1", "alice"], ["2", "bob"]]
e.dumpTable()
dumped = conf.dumper.tableValues[-1]
self.assertEqual(dumped["__infos__"]["count"], 2)
self.assertEqual(dumped["__infos__"]["table"], "users")
self.assertEqual(dumped["__infos__"]["db"], "testdb")
self.assertEqual(list(dumped["id"]["values"]), ["1", "2"])
self.assertEqual(list(dumped["name"]["values"]), ["alice", "bob"])
def test_dump_table_uses_foundData(self):
e = _TestEntries()
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["x"]]
foundData = {"testdb": {"users": {"id": "int"}}}
e.dumpTable(foundData=foundData)
# foundData short-circuits column discovery: getColumns must not run.
self.assertEqual(e.getColumnsCalls, [])
self.assertIn("id", conf.dumper.tableValues[-1])
def test_dump_table_no_columns_skips(self):
e = _TestEntries()
e.getColumnsResult = {} # discovery yields nothing
conf.db = "testdb"
conf.tbl = "ghost"
conf.col = None
emod.inject.getValue = lambda *a, **k: self.fail("should not fetch entries")
e.dumpTable()
# No columns => no values dumped.
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_empty_entries(self):
e = self._entries_with_cols(cols=("id",))
conf.db = "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: None # no rows
e.dumpTable()
# Nothing retrieved => dumpedTable empty => dbTableValues not called.
self.assertEqual(conf.dumper.tableValues, [])
def test_dump_table_current_db(self):
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
conf.db = None # triggers getCurrentDb() -> "testdb"
conf.tbl = "users"
conf.col = None
emod.inject.getValue = lambda *a, **k: [["7"]]
e.dumpTable()
self.assertEqual(conf.db, "testdb")
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["7"])
def test_dump_table_multiple_db_error(self):
e = _TestEntries()
conf.db = "a,b"
conf.tbl = "users"
conf.col = None
from lib.core.exception import SqlmapMissingMandatoryOptionException
self.assertRaises(SqlmapMissingMandatoryOptionException, e.dumpTable)
def test_dump_table_get_tables_when_no_tbl(self):
e = _TestEntries()
e.getTablesResult = {"testdb": ["users"]}
e.getColumnsResult = {"testdb": {"users": {"id": "int"}}}
conf.db = "testdb"
conf.tbl = None
conf.col = None
emod.inject.getValue = lambda *a, **k: [["42"]]
e.dumpTable()
# Tables were discovered via getTables, then the row dumped.
self.assertEqual(list(conf.dumper.tableValues[-1]["id"]["values"]), ["42"])
# --- dumpAll: single-db delegation --------------------------------------
def test_dump_all_single_db_delegates(self):
e = self._entries_with_cols(db="testdb", tbl="users", cols=("id",))
# dumpAll with db set & tbl None must delegate straight to dumpTable.
conf.db = "testdb"
conf.tbl = None
conf.col = None
e.getTablesResult = {"testdb": ["users"]}
emod.inject.getValue = lambda *a, **k: [["9"]]
e.dumpAll()
self.assertTrue(conf.dumper.tableValues)
if __name__ == "__main__":
unittest.main()