From ffffe2d0b201d9b1eaf4c3a821b2ad4c8cd12af0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 28 Jul 2026 16:57:55 +0200 Subject: [PATCH] Fixing multi-column blind --search --- lib/core/settings.py | 2 +- plugins/generic/search.py | 5 ++++- tests/test_search_enum.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index d8823f833..140ef5664 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.223" +VERSION = "1.10.7.224" 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) diff --git a/plugins/generic/search.py b/plugins/generic/search.py index 5ec72f18f..5c444e3db 100644 --- a/plugins/generic/search.py +++ b/plugins/generic/search.py @@ -535,7 +535,10 @@ class Search(object): origDb = conf.db origTbl = conf.tbl - for column, dbData in foundCols.items(): + # Note: only the current column - foundCols also holds columns from earlier + # colList iterations, and re-walking them here re-issued their table lookups + # (O(n^2) blind requests) and duplicated their found tables + for column, dbData in ((column, foundCols[column]),): colQuery = "%s%s" % (colCond, colCondParam) colQuery = colQuery % unsafeSQLIdentificatorNaming(column) diff --git a/tests/test_search_enum.py b/tests/test_search_enum.py index 66b3b850a..a64d290bf 100644 --- a/tests/test_search_enum.py +++ b/tests/test_search_enum.py @@ -372,6 +372,7 @@ class _TestSearchInf(Search): self.like = ('2', "='%s'") # exact match (colConsider '2') self.dumpFoundTablesCalls = [] self.dumpFoundColumnCalls = [] + self.getColumnsCalls = [] def likeOrExact(self, what): return self.like @@ -390,6 +391,7 @@ class _TestSearchInf(Search): def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False): db, tbl, col = conf.db, conf.tbl, conf.col + self.getColumnsCalls.append((db, tbl, col)) if db and tbl: kb.data.cachedColumns.setdefault(db, {}).setdefault(tbl, {}) kb.data.cachedColumns[db][tbl][col] = "varchar" @@ -533,6 +535,35 @@ class TestSearchInference(_SearchBase): self.assertIn("users", dbs["testdb"]) self.assertIn("password", dbs["testdb"]["users"]) + def test_search_column_multi_no_reprocess(self): + # Regression: multi-column blind search must process each column exactly once. + # The nested table-discovery loop used to walk ALL accumulated columns per outer + # pass, re-querying earlier columns (O(n^2) requests) and appending their tables twice. + s = _TestSearchInf() + conf.col = "cola,colb" + conf.db = None + conf.tbl = None + + def gv(query, *a, **k): + if k.get("expected") == EXPECTED.INT: + return "1" # 1 db / 1 table per column + if "table_schema)" in query: # DISTINCT(table_schema) -> db-name fetch + return "testdb" + col = "cola" if "cola" in query else "colb" + return "t_%s" % col # DISTINCT(table_name) -> table-name fetch + smod.inject.getValue = gv + + s.searchColumn() + + # each (db, tbl, col) discovered once - no earlier column re-walked + self.assertEqual(len(s.getColumnsCalls), len(set(s.getColumnsCalls))) + self.assertEqual(len(s.getColumnsCalls), 2) + # and no duplicate table under any found column + foundCols = conf.dumper.dbColumnsArg[0] + for _dbmap in foundCols.values(): + for _tbls in _dbmap.values(): + self.assertEqual(len(_tbls), len(set(_tbls))) + def test_search_column_mysql_lt5_bruteforce_decline(self): s = _TestSearchInf() conf.col = "password"